-
Notifications
You must be signed in to change notification settings - Fork 527
New AI Thinking Control #2543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New AI Thinking Control #2543
Conversation
…e when no premium requests left (defaults to quick)
WalkthroughReplaces "Thinking Level" with "Thinking Mode" across frontend and backend. Frontend: adds a ThinkingLevelDropdown component, updates context menu to set Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (4)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-11-01T00:57:23.025ZApplied to files:
🧬 Code graph analysis (1)pkg/aiusechat/usechat.go (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
🔇 Additional comments (10)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/aiusechat/usechat.go (1)
99-152: Restore premium gating when choosing models.
getWaveAISettingsstill receives thepremiumflag, but the new switch ignores it and happily selectsgpt-5for “balanced”/“deep”. WhenshouldUsePremium()returns false (free users, exhausted allotment, or premium outage), we now attempt a premium call anyway, hitStopKindPremiumRateLimit, and only then retry withgpt-5-mini. That doubles latency, burns retries, and reintroduces the rate-limit storms this guard previously avoided. Please reinstate a server-side fallback (e.g., if!premiumforcethinkingMode = ThinkingModeQuickor overridemodel = DefaultOpenAIModel) before returning the options.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
frontend/app/aipanel/aipanel-contextmenu.ts(3 hunks)frontend/app/aipanel/aipanel.tsx(2 hunks)frontend/app/aipanel/aipanelmessages.tsx(2 hunks)frontend/app/aipanel/thinkinglevel.tsx(1 hunks)frontend/app/aipanel/waveai-model.tsx(3 hunks)frontend/types/gotypes.d.ts(1 hunks)pkg/aiusechat/uctypes/usechat-types.go(1 hunks)pkg/aiusechat/usechat.go(1 hunks)pkg/waveobj/objrtinfo.go(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-01T00:57:23.025Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2504
File: frontend/app/aipanel/aipanel-contextmenu.ts:15-16
Timestamp: 2025-11-01T00:57:23.025Z
Learning: In the waveterm codebase, types defined in custom.d.ts are globally available and do not require explicit imports. Backend types defined in gotypes.d.ts are also globally available.
Applied to files:
frontend/types/gotypes.d.ts
📚 Learning: 2025-10-21T05:09:26.916Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2465
File: frontend/app/onboarding/onboarding-upgrade.tsx:13-21
Timestamp: 2025-10-21T05:09:26.916Z
Learning: In the waveterm codebase, clientData is loaded and awaited in wave.ts before React runs, ensuring it is always available when components mount. This means atoms.client will have data on first render.
Applied to files:
frontend/app/aipanel/aipanel-contextmenu.ts
📚 Learning: 2025-10-15T03:21:02.229Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2433
File: pkg/aiusechat/tools_readfile.go:197-197
Timestamp: 2025-10-15T03:21:02.229Z
Learning: In Wave Terminal's AI tool definitions (pkg/aiusechat/tools_*.go), the Description field should not mention approval requirements even when ToolApproval returns ApprovalNeedsApproval. This prevents the LLM from asking users for approval before calling the tool, avoiding redundant double-approval prompts since the runtime will enforce approval anyway.
Applied to files:
pkg/aiusechat/usechat.go
🧬 Code graph analysis (6)
frontend/app/aipanel/aipanel.tsx (2)
frontend/app/aipanel/aipanel-contextmenu.ts (1)
handleWaveAIContextMenu(12-189)frontend/app/aipanel/thinkinglevel.tsx (1)
ThinkingLevelDropdown(39-118)
frontend/app/aipanel/thinkinglevel.tsx (2)
frontend/app/aipanel/waveai-model.tsx (1)
WaveAIModel(43-577)frontend/app/store/global.ts (1)
atoms(816-816)
frontend/app/aipanel/waveai-model.tsx (3)
frontend/app/store/global.ts (1)
globalStore(839-839)frontend/app/store/wshclientapi.ts (1)
RpcApi(642-642)frontend/app/store/wshrpcutil.ts (1)
TabRpcClient(37-37)
frontend/app/aipanel/aipanel-contextmenu.ts (3)
frontend/app/store/global.ts (1)
atoms(816-816)frontend/app/store/wshclientapi.ts (1)
RpcApi(642-642)frontend/app/store/wshrpcutil.ts (1)
TabRpcClient(37-37)
pkg/aiusechat/usechat.go (1)
pkg/aiusechat/uctypes/usechat-types.go (7)
ThinkingModeBalanced(128-128)ThinkingModeQuick(127-127)DefaultOpenAIModel(14-14)ThinkingLevelLow(121-121)PremiumOpenAIModel(15-15)ThinkingModeDeep(129-129)ThinkingLevelMedium(122-122)
frontend/app/aipanel/aipanelmessages.tsx (1)
frontend/app/aipanel/thinkinglevel.tsx (1)
ThinkingLevelDropdown(39-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build for TestDriver.ai
- GitHub Check: Analyze (go)
- GitHub Check: Analyze (javascript-typescript)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (2)
pkg/aiusechat/usechat.go (1)
121-129: Consider validatingrtInfo.WaveAIThinkingModebefore use.The code correctly restricts non-premium users to Quick mode. However, when
premiumis true andrtInfo.WaveAIThinkingModeis set, there's no validation that it contains a valid mode value (Quick/Balanced/Deep). Invalid mode strings will fall through to the default case in the switch statement below, which grants premium model access.Consider adding validation:
if premium { thinkingMode = uctypes.ThinkingModeBalanced if rtInfo != nil && rtInfo.WaveAIThinkingMode != "" { - thinkingMode = rtInfo.WaveAIThinkingMode + // Validate the thinking mode + switch rtInfo.WaveAIThinkingMode { + case uctypes.ThinkingModeQuick, uctypes.ThinkingModeBalanced, uctypes.ThinkingModeDeep: + thinkingMode = rtInfo.WaveAIThinkingMode + default: + log.Printf("Invalid thinking mode %q in rtInfo, using default: %s", rtInfo.WaveAIThinkingMode, uctypes.ThinkingModeBalanced) + thinkingMode = uctypes.ThinkingModeBalanced + } } } else { thinkingMode = uctypes.ThinkingModeQuick }frontend/app/aipanel/thinkinglevel.tsx (1)
75-114: Missing accessibility features.The dropdown lacks keyboard navigation and ARIA attributes required for accessible UI:
- No
aria-expandedon the trigger button- No
role="menu"on the dropdown container- No
role="menuitem"on options- No keyboard support (arrow keys to navigate, Enter to select, Escape to close)
- No focus management (focus should move to the first enabled option when opened)
Consider adding:
// On the button (line 65): aria-haspopup="menu" aria-expanded={isOpen} // On the dropdown div (line 78): role="menu" // On each option button (line 86): role="menuitem" // Add keyboard handler: onKeyDown={(e) => { if (e.key === "Escape") setIsOpen(false); // ... arrow key navigation logic }}Alternatively, consider using a headless UI library like Radix UI or Headless UI that provides accessible dropdown primitives out of the box.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/app/aipanel/aipanel-contextmenu.ts(3 hunks)frontend/app/aipanel/thinkinglevel.tsx(1 hunks)pkg/aiusechat/usechat.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/app/aipanel/aipanel-contextmenu.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-15T03:21:02.229Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2433
File: pkg/aiusechat/tools_readfile.go:197-197
Timestamp: 2025-10-15T03:21:02.229Z
Learning: In Wave Terminal's AI tool definitions (pkg/aiusechat/tools_*.go), the Description field should not mention approval requirements even when ToolApproval returns ApprovalNeedsApproval. This prevents the LLM from asking users for approval before calling the tool, avoiding redundant double-approval prompts since the runtime will enforce approval anyway.
Applied to files:
pkg/aiusechat/usechat.go
🧬 Code graph analysis (2)
pkg/aiusechat/usechat.go (1)
pkg/aiusechat/uctypes/usechat-types.go (7)
ThinkingModeBalanced(128-128)ThinkingModeQuick(127-127)DefaultOpenAIModel(14-14)ThinkingLevelLow(121-121)PremiumOpenAIModel(15-15)ThinkingModeDeep(129-129)ThinkingLevelMedium(122-122)
frontend/app/aipanel/thinkinglevel.tsx (1)
frontend/app/aipanel/waveai-model.tsx (1)
WaveAIModel(43-577)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build for TestDriver.ai
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
🔇 Additional comments (7)
pkg/aiusechat/usechat.go (1)
111-119: Verify: Should Anthropic also support thinking mode selection?The OpenAI path now supports three thinking modes (Quick, Balanced, Deep) based on premium status and user preferences, but the Anthropic path remains hard-coded to
ThinkingLevelMediumwith no mode selection logic.Is this intentional, or should Anthropic also support the new thinking mode controls? If users select Anthropic as their API provider, they won't benefit from the new thinking mode dropdown feature introduced in this PR.
frontend/app/aipanel/thinkinglevel.tsx (6)
1-7: LGTM!Imports are appropriate for the component's requirements.
9-37: LGTM!Type definitions and metadata are well-structured and align with the PR objectives. The premium flags and model descriptions match the expected behavior.
39-46: LGTM!Component initialization is clean. The premium access logic reasonably treats missing or unknown rate limit data as having premium access.
48-55: LGTM!The selection handler correctly guards premium modes and properly calls the setter in an event handler context (not during render).
63-73: Button rendering is correct, contingent on fixing the state inconsistency.The button properly displays the computed
currentMode, but it inherits the state divergence issue from lines 57-62. Once that's resolved, this rendering logic is sound.
119-119: LGTM!Setting
displayNameis good practice for debugging memoized components.
| let currentMode = (thinkingMode as ThinkingMode) || "balanced"; | ||
| const currentMetadata = ThinkingModeData[currentMode]; | ||
| if (!hasPremium && currentMetadata.premium) { | ||
| currentMode = "quick"; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent UI state when premium mode is restricted.
Three issues:
-
State divergence: When a non-premium user has a premium mode stored in
thinkingMode,currentModeis reassigned to"quick"for display (line 60), but the underlying atom is never updated. The button shows "Quick" but line 84 computesisSelectedfrom the originalthinkingMode, so the dropdown menu highlights the premium mode as selected. This creates a confusing UX. -
Type-safety gap: The cast
(thinkingMode as ThinkingMode)assumes the atom contains a valid mode, but corrupted or stale data could cause a runtime error when accessingThinkingModeData[currentMode]. -
Inconsistent fallback: Falling back to
"balanced"contradicts the PR objective to default non-premium users to "quick".
Recommended fix:
- let currentMode = (thinkingMode as ThinkingMode) || "balanced";
+ const validModes: ThinkingMode[] = ["quick", "balanced", "deep"];
+ let currentMode = validModes.includes(thinkingMode as ThinkingMode)
+ ? (thinkingMode as ThinkingMode)
+ : "quick";
const currentMetadata = ThinkingModeData[currentMode];
if (!hasPremium && currentMetadata.premium) {
currentMode = "quick";
+ // Persist the downgrade if needed
+ if (thinkingMode !== "quick") {
+ model.setThinkingMode("quick");
+ }
}Note: Re-introducing a setter during render requires guarding against redundant calls (as shown above) to avoid duplicate RPCs in React 19 strict mode. Alternatively, use a useEffect with [hasPremium, thinkingMode] dependencies to handle the downgrade asynchronously and keep render pure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
frontend/app/aipanel/thinkingmode.tsx (3)
57-61: State divergence still present: downgraded mode not persisted.This issue was flagged in a previous review. When a non-premium user has a premium mode stored,
currentModeis reassigned to"quick"for display (line 60), but the underlying atom is never updated. The button shows "Quick" while line 84 highlights the stored premium mode as selected, creating a confusing UX.Additionally, line 57 defaults to
"balanced"which contradicts the PR objective to default non-premium users to"quick".As suggested in the previous review, apply this fix:
- let currentMode = (thinkingMode as ThinkingMode) || "balanced"; + const validModes: ThinkingMode[] = ["quick", "balanced", "deep"]; + let currentMode = validModes.includes(thinkingMode as ThinkingMode) + ? (thinkingMode as ThinkingMode) + : "quick"; const currentMetadata = ThinkingModeData[currentMode]; if (!hasPremium && currentMetadata.premium) { currentMode = "quick"; + // Persist the downgrade to avoid state divergence + if (thinkingMode !== "quick") { + model.setThinkingMode("quick"); + } }Note: Calling a setter during render can trigger redundant RPCs in React 19 strict mode. Guard against this by checking
thinkingMode !== "quick"before callingsetThinkingMode, or move the logic to auseEffectwith[hasPremium, thinkingMode]dependencies.
84-84: Selection indicator still uses originalthinkingModeinstead ofcurrentMode.This issue was flagged in a previous review. When a non-premium user has a premium mode stored, the button displays "Quick" (from
currentMode) but this line highlights the stored premium mode as selected because it checksthinkingMode === mode. The checkmark and button label disagree.Fix by comparing against
currentModeinstead:- const isSelected = thinkingMode === mode; + const isSelected = currentMode === mode;This ensures the checkmark appears on the mode shown in the button.
90-91: Invalid Tailwind utility classes still present.This issue was flagged in a previous review. The padding classes
"pt-0.75 pb-0.75"are not valid in Tailwind's default scale, which uses increments of 0.5 (e.g.,pt-1,pt-1.5,pt-2). In Tailwind v4, arbitrary values require bracket syntax:pt-[0.75rem].Apply this fix to use valid Tailwind tokens:
className={`w-full flex flex-col gap-0.5 px-3 ${ - isFirst ? "pt-1.5 pb-0.75" : isLast ? "pt-0.75 pb-1.5" : "pt-0.75 pb-0.75" + isFirst ? "pt-1.5 pb-1" : isLast ? "pt-1 pb-1.5" : "py-1" } ${
🧹 Nitpick comments (1)
frontend/app/aipanel/thinkingmode.tsx (1)
63-115: Consider adding keyboard navigation and ARIA attributes.The dropdown would benefit from accessibility improvements:
- Keyboard navigation (arrow keys to navigate options, Enter to select, Escape to close)
- ARIA attributes:
role="menu",aria-expanded,aria-haspopup- Focus management when opening/closing
These enhancements would improve usability for keyboard and screen reader users.
Example skeleton for keyboard handling:
const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { setIsOpen(false); } // Add arrow key navigation logic };Then add to the button:
<button onClick={() => setIsOpen(!isOpen)} + onKeyDown={handleKeyDown} + aria-haspopup="menu" + aria-expanded={isOpen} className="..."
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/app/aipanel/aipanel.tsx(2 hunks)frontend/app/aipanel/aipanelmessages.tsx(2 hunks)frontend/app/aipanel/thinkingmode.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/app/aipanel/aipanelmessages.tsx
- frontend/app/aipanel/aipanel.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/aipanel/thinkingmode.tsx (1)
frontend/app/aipanel/waveai-model.tsx (1)
WaveAIModel(43-577)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build for TestDriver.ai
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (go)
🔇 Additional comments (1)
frontend/app/aipanel/thinkingmode.tsx (1)
57-57: Default thinking mode must be "quick" for non‑premium requests.thinkingmode.tsx currently falls back to "balanced" then downgrades to "quick" for non‑premium users (state divergence; line ~57). Server and other UI code treat "balanced"/"deep" as premium. Set the canonical defaults to "quick" (source of truth) so non‑premium users never briefly default to a premium mode — change the top-level/default in waveai-model.tsx (atom init) and in loadInitialChat, and update thinkingmode.tsx fallback to "quick".
- Files to change: frontend/app/aipanel/waveai-model.tsx (atom init + loadInitialChat) and frontend/app/aipanel/thinkingmode.tsx (fallback at line ~57).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/app/aipanel/thinkingmode.tsx (1)
63-116: Consider adding keyboard navigation and ARIA attributes.The dropdown currently lacks keyboard accessibility and semantic markup:
- Keyboard navigation: Users cannot navigate menu items with arrow keys, select with Enter, or close with Escape.
- ARIA attributes: Missing
role="listbox",aria-expanded,aria-haspopup="listbox"on the button, androle="option"on menu items.- Focus management: When the dropdown opens, focus remains on the button rather than moving to the menu or first item.
These gaps may prevent keyboard-only users or assistive technology users from effectively using the dropdown, potentially violating WCAG 2.1 Level AA standards.
Example additions:
<button onClick={() => setIsOpen(!isOpen)} + aria-haspopup="listbox" + aria-expanded={isOpen} className="flex items-center gap-1.5 px-2 py-1 text-xs text-gray-300 hover:text-white bg-gray-800/50 hover:bg-gray-700/50 rounded transition-colors cursor-pointer border border-gray-600/50" title={`Thinking: ${currentMetadata.name}`} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setIsOpen(!isOpen); + } + }} > -<div className="absolute top-full right-0 mt-1 bg-gray-800 border border-gray-600 rounded shadow-lg z-50 min-w-[280px]"> +<div role="listbox" className="absolute top-full right-0 mt-1 bg-gray-800 border border-gray-600 rounded shadow-lg z-50 min-w-[280px]"> {(Object.keys(ThinkingModeData) as ThinkingMode[]).map((mode, index) => { ... return ( <button key={mode} + role="option" + aria-selected={isSelected} onClick={() => handleSelect(mode)} disabled={isDisabled} + onKeyDown={(e) => { + if (e.key === "Enter" && !isDisabled) { + e.preventDefault(); + handleSelect(mode); + } else if (e.key === "Escape") { + e.preventDefault(); + setIsOpen(false); + } + }} ...You may also want to add arrow-key navigation to cycle through items and manage focus programmatically when the menu opens.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/app/aipanel/thinkingmode.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/aipanel/thinkingmode.tsx (1)
frontend/app/aipanel/waveai-model.tsx (1)
WaveAIModel(43-577)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Analyze (go)
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Build for TestDriver.ai
| let currentMode = (thinkingMode as ThinkingMode) || "balanced"; | ||
| const currentMetadata = ThinkingModeData[currentMode]; | ||
| if (!hasPremium && currentMetadata.premium) { | ||
| currentMode = "quick"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
State divergence and validation issues remain unresolved.
The concerns raised in previous reviews at lines 57-62 persist:
-
State divergence: When a non-premium user has a premium mode stored in
thinkingMode,currentModeis reassigned to"quick"(line 60), but the atom is never updated viamodel.setThinkingMode("quick"). The dropdown menu correctly usescurrentModefor the selection indicator (line 84), but the underlying atom still holds the premium value, causing inconsistency if other code reads from the atom. -
Type-safety gap: The cast
(thinkingMode as ThinkingMode)on line 57 assumes the atom contains a valid mode. If the atom holds corrupted or stale data (e.g., a deprecated mode name), line 58 will accessundefinedfromThinkingModeData[currentMode], causing a runtime error at line 59 when accessingcurrentMetadata.premium. -
Inconsistent fallback: The default
"balanced"on line 57 contradicts the PR objective to default non-premium users to"quick". Since"balanced"is premium, the immediate reassignment to"quick"at line 60 masks this issue but adds unnecessary logic.
Recommended fix:
- let currentMode = (thinkingMode as ThinkingMode) || "balanced";
+ const validModes: ThinkingMode[] = ["quick", "balanced", "deep"];
+ let currentMode = validModes.includes(thinkingMode as ThinkingMode)
+ ? (thinkingMode as ThinkingMode)
+ : "quick";
const currentMetadata = ThinkingModeData[currentMode];
if (!hasPremium && currentMetadata.premium) {
currentMode = "quick";
+ // Sync atom to match UI if downgraded
+ if (thinkingMode !== "quick") {
+ queueMicrotask(() => model.setThinkingMode("quick"));
+ }
}Note: Using queueMicrotask (or useEffect with [hasPremium, thinkingMode] dependencies) ensures the state update happens after render, avoiding side effects during render which React 19 strict mode double-invokes.
🤖 Prompt for AI Agents
In frontend/app/aipanel/thinkingmode.tsx around lines 57 to 61, the code assigns
currentMode from the atom and silently reassigns it to "quick" for non-premium
users without updating the atom, risks runtime errors by casting without
validation, and uses a contradictory default "balanced"; fix by validating the
atom value against ThinkingModeData and falling back to a non-premium-safe
default ("quick" if !hasPremium, otherwise a known valid default), and when you
change the mode for a non-premium user update the atom via
model.setThinkingMode(...) outside of render (use queueMicrotask or a useEffect
with [hasPremium, thinkingMode] dependencies) so the UI and stored state stay in
sync and you avoid accessing undefined metadata.
Create an AI Thinking Dropdown in Wave AI.
Quick, Balanced, or Deep which map to gpt-5-mini, gpt-5 (low thinking), or gpt-5 (medium thinking). Also default down to Quick when no premium requests.