Skip to content

Conversation

@sawka
Copy link
Member

@sawka sawka commented Nov 11, 2025

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.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

Walkthrough

Replaces "Thinking Level" with "Thinking Mode" across frontend and backend. Frontend: adds a ThinkingLevelDropdown component, updates context menu to set waveai:thinkingmode, renders the dropdown in messages/empty-chat UI, gates premium-only options, and persists mode via RPC. WaveAIModel gains a thinkingMode atom and setThinkingMode. Backend/types: RT info JSON key and struct field changed to waveai:thinkingmode; new thinking mode constants and fields were added to AI option and telemetry structs. AI selection logic now uses thinking mode to determine model and thinking-level mappings.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Files to check closely:
    • frontend/app/aipanel/thinkingmode.tsx — dropdown UI, premium gating, outside-click handling, and state persistence.
    • frontend/app/aipanel/aipanel-contextmenu.ts — RPC payload changes and premium guards.
    • frontend/app/aipanel/aipanel.tsx and frontend/app/aipanel/aipanelmessages.tsx — rendering/positioning and CSS context.
    • frontend/app/aipanel/waveai-model.tsx — new atom and RPC persistence behavior.
    • frontend/types/gotypes.d.ts and pkg/waveobj/objrtinfo.go — RT info field and JSON tag rename consistency.
    • pkg/aiusechat/uctypes/usechat-types.go and pkg/aiusechat/usechat.go — thinking-mode constants, propagation into AI options, model selection mapping, and telemetry propagation.
    • pkg/telemetry/telemetrydata/telemetrydata.go — added telemetry fields and JSON tags.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'New AI Thinking Control' accurately reflects the main change: introducing a dropdown control for AI thinking modes (Quick, Balanced, Deep).
Description check ✅ Passed The description clearly relates to the changeset, explaining the dropdown functionality, mode mappings, and premium-gating behavior implemented across the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/ai-thinking

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f3a972 and 7322d82.

📒 Files selected for processing (4)
  • frontend/types/gotypes.d.ts (2 hunks)
  • pkg/aiusechat/uctypes/usechat-types.go (3 hunks)
  • pkg/aiusechat/usechat.go (3 hunks)
  • pkg/telemetry/telemetrydata/telemetrydata.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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
🧬 Code graph analysis (1)
pkg/aiusechat/usechat.go (1)
pkg/aiusechat/uctypes/usechat-types.go (9)
  • ThinkingModeBalanced (128-128)
  • ThinkingModeQuick (127-127)
  • ThinkingLevelMedium (122-122)
  • AIOptsType (210-222)
  • DefaultAnthropicModel (13-13)
  • DefaultOpenAIModel (14-14)
  • ThinkingLevelLow (121-121)
  • PremiumOpenAIModel (15-15)
  • ThinkingModeDeep (129-129)
⏰ 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 (javascript-typescript)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (10)
pkg/telemetry/telemetrydata/telemetrydata.go (1)

147-148: LGTM!

The new telemetry fields for thinking level and thinking mode follow existing naming conventions and integrate cleanly with the telemetry structure.

frontend/types/gotypes.d.ts (2)

856-856: LGTM!

The ObjRTInfo type correctly reflects the backend change from waveai:thinkinglevel to waveai:thinkingmode.


1148-1149: LGTM!

The telemetry event props correctly include both thinking level and thinking mode fields, consistent with the backend telemetry structure.

pkg/aiusechat/uctypes/usechat-types.go (3)

126-130: LGTM!

The thinking mode constants are well-defined and follow existing conventions for the thinking level constants.


221-221: LGTM!

The ThinkingMode field is properly added to AIOptsType with appropriate JSON tags and documentation.


264-265: LGTM!

The thinking level and mode fields are appropriately added to AIMetrics for telemetry tracking.

pkg/aiusechat/usechat.go (4)

120-129: Anthropic always uses medium thinking level.

Unlike the OpenAI path which varies thinking level based on thinking mode (low for Quick/Balanced, medium for Deep), the Anthropic path always uses ThinkingLevelMedium regardless of the thinking mode. Verify this is intentional.


418-421: LGTM!

The metrics initialization correctly captures thinking level and mode from the chat configuration.


633-634: LGTM!

The telemetry event correctly includes thinking level and mode from the metrics.


111-119: Backend correctly gates non-premium users to Quick mode — no changes needed.

The code properly enforces this behavior. The premium flag derives from server-authoritative rate limit info updated after API responses, not client input. The rtInfo override (line 115) only applies when premium=true, and non-premium users are forced to Quick mode at line 118 regardless of rtInfo content. This is intentional feature gating with proper server-side enforcement.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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.

getWaveAISettings still receives the premium flag, but the new switch ignores it and happily selects gpt-5 for “balanced”/“deep”. When shouldUsePremium() returns false (free users, exhausted allotment, or premium outage), we now attempt a premium call anyway, hit StopKindPremiumRateLimit, and only then retry with gpt-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 !premium force thinkingMode = ThinkingModeQuick or override model = DefaultOpenAIModel) before returning the options.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7955bf6 and 32b065b.

📒 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)

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 validating rtInfo.WaveAIThinkingMode before use.

The code correctly restricts non-premium users to Quick mode. However, when premium is true and rtInfo.WaveAIThinkingMode is 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-expanded on 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

📥 Commits

Reviewing files that changed from the base of the PR and between 32b065b and 13920cb.

📒 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 ThinkingLevelMedium with 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 displayName is good practice for debugging memoized components.

Comment on lines +57 to +62
let currentMode = (thinkingMode as ThinkingMode) || "balanced";
const currentMetadata = ThinkingModeData[currentMode];
if (!hasPremium && currentMetadata.premium) {
currentMode = "quick";
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent UI state when premium mode is restricted.

Three issues:

  1. State divergence: When a non-premium user has a premium mode stored in thinkingMode, currentMode is reassigned to "quick" for display (line 60), but the underlying atom is never updated. The button shows "Quick" but line 84 computes isSelected from the original thinkingMode, so the dropdown menu highlights the premium mode as selected. This creates a confusing UX.

  2. 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 accessing ThinkingModeData[currentMode].

  3. 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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, currentMode is 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 calling setThinkingMode, or move the logic to a useEffect with [hasPremium, thinkingMode] dependencies.


84-84: Selection indicator still uses original thinkingMode instead of currentMode.

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 checks thinkingMode === mode. The checkmark and button label disagree.

Fix by comparing against currentMode instead:

-                            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

📥 Commits

Reviewing files that changed from the base of the PR and between 13920cb and 3f280e1.

📒 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).

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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, and role="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

📥 Commits

Reviewing files that changed from the base of the PR and between 3f280e1 and 6f3a972.

📒 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

Comment on lines +57 to +61
let currentMode = (thinkingMode as ThinkingMode) || "balanced";
const currentMetadata = ThinkingModeData[currentMode];
if (!hasPremium && currentMetadata.premium) {
currentMode = "quick";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

State divergence and validation issues remain unresolved.

The concerns raised in previous reviews at lines 57-62 persist:

  1. State divergence: When a non-premium user has a premium mode stored in thinkingMode, currentMode is reassigned to "quick" (line 60), but the atom is never updated via model.setThinkingMode("quick"). The dropdown menu correctly uses currentMode for the selection indicator (line 84), but the underlying atom still holds the premium value, causing inconsistency if other code reads from the atom.

  2. 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 access undefined from ThinkingModeData[currentMode], causing a runtime error at line 59 when accessing currentMetadata.premium.

  3. 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.

@sawka sawka merged commit 4da6a39 into main Nov 12, 2025
6 of 8 checks passed
@sawka sawka deleted the sawka/ai-thinking branch November 12, 2025 03:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants