From f7fcd1688adfd6e4223452b8318239997ca043c7 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 10 Nov 2025 22:26:56 -0800 Subject: [PATCH 1/6] new thinking level dropdown in wave ai --- frontend/app/aipanel/aipanel-contextmenu.ts | 26 +++--- frontend/app/aipanel/aipanel.tsx | 6 +- frontend/app/aipanel/aipanelmessages.tsx | 6 +- frontend/app/aipanel/thinkinglevel.tsx | 93 +++++++++++++++++++++ frontend/app/aipanel/waveai-model.tsx | 12 +++ frontend/types/gotypes.d.ts | 2 +- pkg/aiusechat/usechat.go | 8 +- pkg/waveobj/objrtinfo.go | 2 +- 8 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 frontend/app/aipanel/thinkinglevel.tsx diff --git a/frontend/app/aipanel/aipanel-contextmenu.ts b/frontend/app/aipanel/aipanel-contextmenu.ts index a783af7a4c..4aaa02fabe 100644 --- a/frontend/app/aipanel/aipanel-contextmenu.ts +++ b/frontend/app/aipanel/aipanel-contextmenu.ts @@ -38,41 +38,41 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo oref: model.orefContext, }); - const currentThinkingLevel = rtInfo?.["waveai:thinkinglevel"] ?? "medium"; + const currentThinkingMode = rtInfo?.["waveai:thinkingmode"] ?? "balanced"; const defaultTokens = model.inBuilder ? 24576 : 4096; const currentMaxTokens = rtInfo?.["waveai:maxoutputtokens"] ?? defaultTokens; - const thinkingLevelSubmenu: ContextMenuItem[] = [ + const thinkingModeSubmenu: ContextMenuItem[] = [ { - label: "Low", + label: "Quick (gpt-5-mini)", type: "checkbox", - checked: currentThinkingLevel === "low", + checked: currentThinkingMode === "quick", click: () => { RpcApi.SetRTInfoCommand(TabRpcClient, { oref: model.orefContext, - data: { "waveai:thinkinglevel": "low" }, + data: { "waveai:thinkingmode": "quick" }, }); }, }, { - label: "Medium", + label: "Balanced (gpt-5, low thinking)", type: "checkbox", - checked: currentThinkingLevel === "medium", + checked: currentThinkingMode === "balanced", click: () => { RpcApi.SetRTInfoCommand(TabRpcClient, { oref: model.orefContext, - data: { "waveai:thinkinglevel": "medium" }, + data: { "waveai:thinkingmode": "balanced" }, }); }, }, { - label: "High", + label: "Deep (gpt-5, full thinking)", type: "checkbox", - checked: currentThinkingLevel === "high", + checked: currentThinkingMode === "deep", click: () => { RpcApi.SetRTInfoCommand(TabRpcClient, { oref: model.orefContext, - data: { "waveai:thinkinglevel": "high" }, + data: { "waveai:thinkingmode": "deep" }, }); }, }, @@ -157,8 +157,8 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo } menu.push({ - label: "Thinking Level", - submenu: thinkingLevelSubmenu, + label: "Thinking Mode", + submenu: thinkingModeSubmenu, }); menu.push({ diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 7a2c7e3b11..fa57cf9f0a 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -21,6 +21,7 @@ import { AIPanelInput } from "./aipanelinput"; import { AIPanelMessages } from "./aipanelmessages"; import { AIRateLimitStrip } from "./airatelimitstrip"; import { TelemetryRequiredMessage } from "./telemetryrequired"; +import { ThinkingLevelDropdown } from "./thinkinglevel"; import { WaveAIModel } from "./waveai-model"; const AIBlockMask = memo(() => { @@ -489,9 +490,12 @@ const AIPanelComponentInner = memo(() => { <> {messages.length === 0 && initialLoadDone ? (
handleWaveAIContextMenu(e, true)} > +
+ +
{model.inBuilder ? : }
) : ( diff --git a/frontend/app/aipanel/aipanelmessages.tsx b/frontend/app/aipanel/aipanelmessages.tsx index 781ce73fd8..d1de4cdac4 100644 --- a/frontend/app/aipanel/aipanelmessages.tsx +++ b/frontend/app/aipanel/aipanelmessages.tsx @@ -4,6 +4,7 @@ import { useAtomValue } from "jotai"; import { memo, useEffect, useRef } from "react"; import { AIMessage } from "./aimessage"; +import { ThinkingLevelDropdown } from "./thinkinglevel"; import { WaveAIModel } from "./waveai-model"; interface AIPanelMessagesProps { @@ -41,7 +42,10 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane }, [isPanelOpen]); return ( -
+
+
+ +
{messages.map((message, index) => { const isLastMessage = index === messages.length - 1; const isStreaming = status === "streaming" && isLastMessage && message.role === "assistant"; diff --git a/frontend/app/aipanel/thinkinglevel.tsx b/frontend/app/aipanel/thinkinglevel.tsx new file mode 100644 index 0000000000..91dd1f816c --- /dev/null +++ b/frontend/app/aipanel/thinkinglevel.tsx @@ -0,0 +1,93 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useAtomValue } from "jotai"; +import { memo, useRef, useState } from "react"; +import { WaveAIModel } from "./waveai-model"; + +type ThinkingMode = "quick" | "balanced" | "deep"; + +interface ThinkingModeMetadata { + icon: string; + name: string; + desc: string; +} + +const ThinkingModeData: Record = { + quick: { + icon: "fa-bolt", + name: "Quick", + desc: "Fastest responses (gpt-5-mini)", + }, + balanced: { + icon: "fa-sparkles", + name: "Balanced", + desc: "Good mix of speed and accuracy\n(gpt-5 with minimal thinking)", + }, + deep: { + icon: "fa-lightbulb", + name: "Deep", + desc: "Slower but most capable\n(gpt-5 with full reasoning)", + }, +}; + +export const ThinkingLevelDropdown = memo(() => { + const model = WaveAIModel.getInstance(); + const thinkingMode = useAtomValue(model.thinkingMode); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const handleSelect = (mode: ThinkingMode) => { + model.setThinkingMode(mode); + setIsOpen(false); + }; + + const currentMode = (thinkingMode as ThinkingMode) || "balanced"; + const currentMetadata = ThinkingModeData[currentMode]; + + return ( +
+ + + {isOpen && ( + <> +
setIsOpen(false)} /> +
+ {(Object.keys(ThinkingModeData) as ThinkingMode[]).map((mode, index) => { + const metadata = ThinkingModeData[mode]; + const isLast = index === Object.keys(ThinkingModeData).length - 1; + return ( + + ); + })} +
+ + )} +
+ ); +}); + +ThinkingLevelDropdown.displayName = "ThinkingLevelDropdown"; diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index 3f101ec618..f980aa8887 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -56,6 +56,7 @@ export class WaveAIModel { widgetAccessAtom!: jotai.Atom; droppedFiles: jotai.PrimitiveAtom = jotai.atom([]); chatId!: jotai.PrimitiveAtom; + thinkingMode: jotai.PrimitiveAtom = jotai.atom("balanced"); errorMessage: jotai.PrimitiveAtom = jotai.atom(null) as jotai.PrimitiveAtom; modelAtom!: jotai.Atom; containerWidth: jotai.PrimitiveAtom = jotai.atom(0); @@ -331,6 +332,14 @@ export class WaveAIModel { }); } + setThinkingMode(mode: string) { + globalStore.set(this.thinkingMode, mode); + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: this.orefContext, + data: { "waveai:thinkingmode": mode }, + }); + } + async loadInitialChat(): Promise { const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref: this.orefContext, @@ -345,6 +354,9 @@ export class WaveAIModel { } globalStore.set(this.chatId, chatIdValue); + const thinkingModeValue = rtInfo?.["waveai:thinkingmode"] ?? "balanced"; + globalStore.set(this.thinkingMode, thinkingModeValue); + try { const chatData = await RpcApi.GetWaveAIChatCommand(TabRpcClient, { chatid: chatIdValue }); const messages: UIMessage[] = chatData?.messages ?? []; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index b6915ef903..aa7d8f9e73 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -853,7 +853,7 @@ declare global { "builder:appid"?: string; "builder:env"?: {[key: string]: string}; "waveai:chatid"?: string; - "waveai:thinkinglevel"?: string; + "waveai:thinkingmode"?: string; "waveai:maxoutputtokens"?: number; }; diff --git a/pkg/aiusechat/usechat.go b/pkg/aiusechat/usechat.go index 70e07a1baa..41f0726249 100644 --- a/pkg/aiusechat/usechat.go +++ b/pkg/aiusechat/usechat.go @@ -110,8 +110,8 @@ func getWaveAISettings(premium bool, builderMode bool, rtInfo *waveobj.ObjRTInfo } if DefaultAPI == APIType_Anthropic { thinkingLevel := uctypes.ThinkingLevelMedium - if rtInfo != nil && rtInfo.WaveAIThinkingLevel != "" { - thinkingLevel = rtInfo.WaveAIThinkingLevel + if rtInfo != nil && rtInfo.WaveAIThinkingMode != "" { + thinkingLevel = rtInfo.WaveAIThinkingMode } return &uctypes.AIOptsType{ APIType: APIType_Anthropic, @@ -126,8 +126,8 @@ func getWaveAISettings(premium bool, builderMode bool, rtInfo *waveobj.ObjRTInfo if premium { model = uctypes.PremiumOpenAIModel thinkingLevel = uctypes.ThinkingLevelMedium - if rtInfo != nil && rtInfo.WaveAIThinkingLevel != "" { - thinkingLevel = rtInfo.WaveAIThinkingLevel + if rtInfo != nil && rtInfo.WaveAIThinkingMode != "" { + thinkingLevel = rtInfo.WaveAIThinkingMode } } return &uctypes.AIOptsType{ diff --git a/pkg/waveobj/objrtinfo.go b/pkg/waveobj/objrtinfo.go index d484c8f63d..30afe48b07 100644 --- a/pkg/waveobj/objrtinfo.go +++ b/pkg/waveobj/objrtinfo.go @@ -23,6 +23,6 @@ type ObjRTInfo struct { BuilderEnv map[string]string `json:"builder:env,omitempty"` WaveAIChatId string `json:"waveai:chatid,omitempty"` - WaveAIThinkingLevel string `json:"waveai:thinkinglevel,omitempty"` + WaveAIThinkingMode string `json:"waveai:thinkingmode,omitempty"` WaveAIMaxOutputTokens int `json:"waveai:maxoutputtokens,omitempty"` } From 32b065b11de90eee5aec1a4192cd6d5e5ec3e9a3 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 10 Nov 2025 22:43:32 -0800 Subject: [PATCH 2/6] hook up thinking level to backend / models (default balanced). disable when no premium requests left (defaults to quick) --- frontend/app/aipanel/aipanel-contextmenu.ts | 14 +++++++-- frontend/app/aipanel/thinkinglevel.tsx | 35 ++++++++++++++++++--- pkg/aiusechat/uctypes/usechat-types.go | 6 ++++ pkg/aiusechat/usechat.go | 29 +++++++++++------ 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/frontend/app/aipanel/aipanel-contextmenu.ts b/frontend/app/aipanel/aipanel-contextmenu.ts index 4aaa02fabe..15cdb622aa 100644 --- a/frontend/app/aipanel/aipanel-contextmenu.ts +++ b/frontend/app/aipanel/aipanel-contextmenu.ts @@ -3,7 +3,8 @@ import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils"; import { ContextMenuModel } from "@/app/store/contextmenu"; -import { isDev } from "@/app/store/global"; +import { atoms, isDev } from "@/app/store/global"; +import { globalStore } from "@/app/store/jotaiStore"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { WaveAIModel } from "./waveai-model"; @@ -42,6 +43,9 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo const defaultTokens = model.inBuilder ? 24576 : 4096; const currentMaxTokens = rtInfo?.["waveai:maxoutputtokens"] ?? defaultTokens; + const rateLimitInfo = globalStore.get(atoms.waveAIRateLimitInfoAtom); + const hasPremium = !rateLimitInfo || rateLimitInfo.unknown || rateLimitInfo.preq > 0; + const thinkingModeSubmenu: ContextMenuItem[] = [ { label: "Quick (gpt-5-mini)", @@ -55,10 +59,12 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo }, }, { - label: "Balanced (gpt-5, low thinking)", + label: hasPremium ? "Balanced (gpt-5, low thinking)" : "Balanced (premium)", type: "checkbox", checked: currentThinkingMode === "balanced", + enabled: hasPremium, click: () => { + if (!hasPremium) return; RpcApi.SetRTInfoCommand(TabRpcClient, { oref: model.orefContext, data: { "waveai:thinkingmode": "balanced" }, @@ -66,10 +72,12 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo }, }, { - label: "Deep (gpt-5, full thinking)", + label: hasPremium ? "Deep (gpt-5, full thinking)" : "Deep (premium)", type: "checkbox", checked: currentThinkingMode === "deep", + enabled: hasPremium, click: () => { + if (!hasPremium) return; RpcApi.SetRTInfoCommand(TabRpcClient, { oref: model.orefContext, data: { "waveai:thinkingmode": "deep" }, diff --git a/frontend/app/aipanel/thinkinglevel.tsx b/frontend/app/aipanel/thinkinglevel.tsx index 91dd1f816c..bcd0eb7e9a 100644 --- a/frontend/app/aipanel/thinkinglevel.tsx +++ b/frontend/app/aipanel/thinkinglevel.tsx @@ -1,6 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { atoms } from "@/app/store/global"; import { useAtomValue } from "jotai"; import { memo, useRef, useState } from "react"; import { WaveAIModel } from "./waveai-model"; @@ -11,6 +12,7 @@ interface ThinkingModeMetadata { icon: string; name: string; desc: string; + premium: boolean; } const ThinkingModeData: Record = { @@ -18,32 +20,46 @@ const ThinkingModeData: Record = { icon: "fa-bolt", name: "Quick", desc: "Fastest responses (gpt-5-mini)", + premium: false, }, balanced: { icon: "fa-sparkles", name: "Balanced", desc: "Good mix of speed and accuracy\n(gpt-5 with minimal thinking)", + premium: true, }, deep: { icon: "fa-lightbulb", name: "Deep", desc: "Slower but most capable\n(gpt-5 with full reasoning)", + premium: true, }, }; export const ThinkingLevelDropdown = memo(() => { const model = WaveAIModel.getInstance(); const thinkingMode = useAtomValue(model.thinkingMode); + const rateLimitInfo = useAtomValue(atoms.waveAIRateLimitInfoAtom); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); + const hasPremium = !rateLimitInfo || rateLimitInfo.unknown || rateLimitInfo.preq > 0; + const handleSelect = (mode: ThinkingMode) => { + const metadata = ThinkingModeData[mode]; + if (!hasPremium && metadata.premium) { + return; + } model.setThinkingMode(mode); setIsOpen(false); }; - const currentMode = (thinkingMode as ThinkingMode) || "balanced"; + let currentMode = (thinkingMode as ThinkingMode) || "balanced"; const currentMetadata = ThinkingModeData[currentMode]; + if (!hasPremium && currentMetadata.premium) { + currentMode = "quick"; + model.setThinkingMode("quick"); + } return (
@@ -63,19 +79,30 @@ export const ThinkingLevelDropdown = memo(() => {
{(Object.keys(ThinkingModeData) as ThinkingMode[]).map((mode, index) => { const metadata = ThinkingModeData[mode]; + const isFirst = index === 0; const isLast = index === Object.keys(ThinkingModeData).length - 1; + const isDisabled = !hasPremium && metadata.premium; + const isSelected = thinkingMode === mode; return (