diff --git a/emain/emain-window.ts b/emain/emain-window.ts index f9d13df2b8..059d7e39b7 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -25,6 +25,7 @@ import { updater } from "./updater"; export type WindowOpts = { unamePlatform: string; isPrimaryStartupWindow?: boolean; + foregroundWindow?: boolean; }; export const MinWindowWidth = 800; @@ -193,7 +194,7 @@ export class WaveBrowserWindow extends BaseWindow { super(winOpts); const fullscreenOnLaunch = fullConfig?.settings["window:fullscreenonlaunch"]; - if (fullscreenOnLaunch) { + if (fullscreenOnLaunch && opts.foregroundWindow) { this.once("show", () => { this.setFullScreen(true); }); @@ -852,6 +853,7 @@ export async function relaunchBrowserWindows() { const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform, isPrimaryStartupWindow, + foregroundWindow: windowId === primaryWindowId, }); wins.push(win); } diff --git a/frontend/app/aipanel/aipanel-contextmenu.ts b/frontend/app/aipanel/aipanel-contextmenu.ts new file mode 100644 index 0000000000..db6edc9577 --- /dev/null +++ b/frontend/app/aipanel/aipanel-contextmenu.ts @@ -0,0 +1,163 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils"; +import { ContextMenuModel } from "@/app/store/contextmenu"; +import { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { WaveAIModel } from "./waveai-model"; + +export async function handleWaveAIContextMenu(e: React.MouseEvent, onClose?: () => void): Promise { + e.preventDefault(); + e.stopPropagation(); + + const model = WaveAIModel.getInstance(); + const menu: ContextMenuItem[] = []; + + const hasSelection = waveAIHasSelection(); + if (hasSelection) { + menu.push({ + role: "copy", + }); + menu.push({ type: "separator" }); + } + + menu.push({ + label: "New Chat", + click: () => { + model.clearChat(); + }, + }); + + menu.push({ type: "separator" }); + + const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + }); + + const currentThinkingLevel = rtInfo?.["waveai:thinkinglevel"] ?? "medium"; + const defaultTokens = model.inBuilder ? 24576 : 4096; + const currentMaxTokens = rtInfo?.["waveai:maxoutputtokens"] ?? defaultTokens; + + const thinkingLevelSubmenu: ContextMenuItem[] = [ + { + label: "Low", + type: "checkbox", + checked: currentThinkingLevel === "low", + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:thinkinglevel": "low" }, + }); + }, + }, + { + label: "Medium", + type: "checkbox", + checked: currentThinkingLevel === "medium", + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:thinkinglevel": "medium" }, + }); + }, + }, + { + label: "High", + type: "checkbox", + checked: currentThinkingLevel === "high", + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:thinkinglevel": "high" }, + }); + }, + }, + ]; + + const maxTokensSubmenu: ContextMenuItem[] = []; + + if (model.inBuilder) { + maxTokensSubmenu.push( + { + label: "24k", + type: "checkbox", + checked: currentMaxTokens === 24576, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 24576 }, + }); + }, + }, + { + label: "64k (Pro)", + type: "checkbox", + checked: currentMaxTokens === 65536, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 65536 }, + }); + }, + } + ); + } else { + maxTokensSubmenu.push( + { + label: "4k", + type: "checkbox", + checked: currentMaxTokens === 4096, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 4096 }, + }); + }, + }, + { + label: "16k (Pro)", + type: "checkbox", + checked: currentMaxTokens === 16384, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 16384 }, + }); + }, + }, + { + label: "64k (Pro)", + type: "checkbox", + checked: currentMaxTokens === 65536, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 65536 }, + }); + }, + } + ); + } + + menu.push({ + label: "Thinking Level", + submenu: thinkingLevelSubmenu, + }); + + menu.push({ + label: "Max Output Tokens", + submenu: maxTokensSubmenu, + }); + + menu.push({ type: "separator" }); + + menu.push({ + label: "Hide Wave AI", + click: () => { + onClose?.(); + }, + }); + + ContextMenuModel.showContextMenu(menu, e); +} \ No newline at end of file diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 43ace431b4..6ef092909d 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -1,9 +1,9 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { handleWaveAIContextMenu } from "@/app/aipanel/aipanel-contextmenu"; import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils"; import { ErrorBoundary } from "@/app/element/errorboundary"; -import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, getSettingsKeyAtom } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; @@ -204,11 +204,10 @@ const AIErrorMessage = memo(({ errorMessage, onClear }: AIErrorMessageProps) => AIErrorMessage.displayName = "AIErrorMessage"; interface AIPanelProps { - className?: string; onClose?: () => void; } -const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { +const AIPanelComponentInner = memo(({ onClose }: AIPanelProps) => { const [isDragOver, setIsDragOver] = useState(false); const [isReactDndDragOver, setIsReactDndDragOver] = useState(false); const [initialLoadDone, setInitialLoadDone] = useState(false); @@ -467,39 +466,6 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { }, 0); }; - const handleMessagesContextMenu = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const menu: ContextMenuItem[] = []; - - const hasSelection = waveAIHasSelection(); - if (hasSelection) { - menu.push({ - role: "copy", - }); - menu.push({ type: "separator" }); - } - - menu.push({ - label: "New Chat", - click: () => { - model.clearChat(); - }, - }); - - menu.push({ type: "separator" }); - - menu.push({ - label: "Hide Wave AI", - click: () => { - onClose?.(); - }, - }); - - ContextMenuModel.showContextMenu(menu, e); - }; - const showBlockMask = isLayoutMode && showOverlayBlockNums; return ( @@ -507,9 +473,8 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { ref={containerRef} data-waveai-panel="true" className={cn( - "bg-gray-900 flex flex-col relative h-[calc(100%-4px)]", - model.inBuilder ? "mt-0" : "mt-1", - className, + "bg-gray-900 flex flex-col relative", + model.inBuilder ? "mt-0 h-full" : "mt-1 h-[calc(100%-4px)]", (isDragOver || isReactDndDragOver) && "bg-gray-800 border-accent", isFocused ? "border-2 border-accent" : "border-2 border-transparent" )} @@ -537,14 +502,17 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { ) : ( <> {messages.length === 0 && initialLoadDone ? ( -
+
handleWaveAIContextMenu(e, onClose)} + > {model.inBuilder ? : }
) : ( handleWaveAIContextMenu(e, onClose)} /> )} {errorMessage && ( @@ -561,10 +529,10 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { AIPanelComponentInner.displayName = "AIPanelInner"; -const AIPanelComponent = ({ className, onClose }: AIPanelProps) => { +const AIPanelComponent = ({ onClose }: AIPanelProps) => { return ( - + ); }; diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index c2ba16a794..3daeb4872c 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -50,7 +50,7 @@ export class WaveAIModel { private useChatStop: (() => void) | null = null; // Used for injecting Wave-specific message data into DefaultChatTransport's prepareSendMessagesRequest realMessage: AIMessage | null = null; - private orefContext: ORef; + orefContext: ORef; inBuilder: boolean = false; widgetAccessAtom!: jotai.Atom; @@ -65,7 +65,9 @@ export class WaveAIModel { isChatEmpty: boolean = true; isWaveAIFocusedAtom!: jotai.Atom; panelVisibleAtom!: jotai.Atom; - restoreBackupModalToolCallId: jotai.PrimitiveAtom = jotai.atom(null) as jotai.PrimitiveAtom; + restoreBackupModalToolCallId: jotai.PrimitiveAtom = jotai.atom(null) as jotai.PrimitiveAtom< + string | null + >; restoreBackupStatus: jotai.PrimitiveAtom<"idle" | "processing" | "success" | "error"> = jotai.atom("idle"); restoreBackupError: jotai.PrimitiveAtom = jotai.atom(null) as jotai.PrimitiveAtom; diff --git a/frontend/builder/builder-app.tsx b/frontend/builder/builder-app.tsx index af5c7acd28..dea4c9dbf6 100644 --- a/frontend/builder/builder-app.tsx +++ b/frontend/builder/builder-app.tsx @@ -9,6 +9,8 @@ import * as keyutil from "@/util/keyutil"; import { isBlank } from "@/util/util"; import { Provider, useAtomValue } from "jotai"; import { useEffect } from "react"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; type BuilderAppProps = { initOpts: BuilderInitOpts; @@ -41,7 +43,9 @@ function BuilderAppInner() { WaveApp Builder{!isBlank(builderAppId) && ` (${builderAppId})`}
- {isBlank(builderAppId) ? : } + + {isBlank(builderAppId) ? : } + ); } diff --git a/frontend/builder/builder-workspace.tsx b/frontend/builder/builder-workspace.tsx index 3b1a84e412..395fdb32a1 100644 --- a/frontend/builder/builder-workspace.tsx +++ b/frontend/builder/builder-workspace.tsx @@ -97,7 +97,7 @@ const BuilderWorkspace = memo(() => {
- + diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 199fe0be0d..721f913cd6 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -819,6 +819,8 @@ declare global { "builder:appid"?: string; "builder:env"?: {[key: string]: string}; "waveai:chatid"?: string; + "waveai:thinkinglevel"?: string; + "waveai:maxoutputtokens"?: number; }; // iochantypes.Packet diff --git a/pkg/aiusechat/openai/openai-convertmessage.go b/pkg/aiusechat/openai/openai-convertmessage.go index f0d3c47c57..78f91e7b5d 100644 --- a/pkg/aiusechat/openai/openai-convertmessage.go +++ b/pkg/aiusechat/openai/openai-convertmessage.go @@ -137,11 +137,22 @@ func debugPrintReq(req *OpenAIRequest, endpoint string) { for _, tool := range req.Tools { toolNames = append(toolNames, tool.Name) } - log.Printf("model %s\n", req.Model) + modelInfo := req.Model + var details []string + if req.Reasoning != nil && req.Reasoning.Effort != "" { + details = append(details, fmt.Sprintf("reasoning: %s", req.Reasoning.Effort)) + } + if req.MaxOutputTokens > 0 { + details = append(details, fmt.Sprintf("max_tokens: %d", req.MaxOutputTokens)) + } + if len(details) > 0 { + log.Printf("model %s (%s)\n", modelInfo, strings.Join(details, ", ")) + } else { + log.Printf("model %s\n", modelInfo) + } if len(toolNames) > 0 { log.Printf("tools: %s\n", strings.Join(toolNames, ",")) } - // log.Printf("reasoning %v\n", req.Reasoning) log.Printf("inputs (%d):", len(req.Input)) for idx, input := range req.Input { diff --git a/pkg/aiusechat/usechat.go b/pkg/aiusechat/usechat.go index 2d75bc4c1f..f00463c2e4 100644 --- a/pkg/aiusechat/usechat.go +++ b/pkg/aiusechat/usechat.go @@ -151,7 +151,7 @@ var BuilderSystemPromptText_OpenAI = strings.Join([]string{ ``, }, "\n") -func getWaveAISettings(premium bool, builderMode bool) (*uctypes.AIOptsType, error) { +func getWaveAISettings(premium bool, builderMode bool, rtInfo *waveobj.ObjRTInfo) (*uctypes.AIOptsType, error) { baseUrl := DefaultAIEndpoint if os.Getenv("WAVETERM_WAVEAI_ENDPOINT") != "" { baseUrl = os.Getenv("WAVETERM_WAVEAI_ENDPOINT") @@ -160,12 +160,19 @@ func getWaveAISettings(premium bool, builderMode bool) (*uctypes.AIOptsType, err if builderMode { maxTokens = BuilderMaxTokens } + if rtInfo != nil && rtInfo.WaveAIMaxOutputTokens > 0 { + maxTokens = rtInfo.WaveAIMaxOutputTokens + } if DefaultAPI == APIType_Anthropic { + thinkingLevel := uctypes.ThinkingLevelMedium + if rtInfo != nil && rtInfo.WaveAIThinkingLevel != "" { + thinkingLevel = rtInfo.WaveAIThinkingLevel + } return &uctypes.AIOptsType{ APIType: APIType_Anthropic, Model: uctypes.DefaultAnthropicModel, MaxTokens: maxTokens, - ThinkingLevel: uctypes.ThinkingLevelMedium, + ThinkingLevel: thinkingLevel, BaseURL: baseUrl, }, nil } else if DefaultAPI == APIType_OpenAI { @@ -174,6 +181,9 @@ func getWaveAISettings(premium bool, builderMode bool) (*uctypes.AIOptsType, err if premium { model = uctypes.PremiumOpenAIModel thinkingLevel = uctypes.ThinkingLevelMedium + if rtInfo != nil && rtInfo.WaveAIThinkingLevel != "" { + thinkingLevel = rtInfo.WaveAIThinkingLevel + } } return &uctypes.AIOptsType{ APIType: APIType_OpenAI, @@ -685,10 +695,20 @@ func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) { return } + // Get RTInfo from TabId or BuilderId + var rtInfo *waveobj.ObjRTInfo + if req.TabId != "" { + oref := waveobj.MakeORef(waveobj.OType_Tab, req.TabId) + rtInfo = wstore.GetRTInfo(oref) + } else if req.BuilderId != "" { + oref := waveobj.MakeORef(waveobj.OType_Builder, req.BuilderId) + rtInfo = wstore.GetRTInfo(oref) + } + // Get WaveAI settings premium := shouldUsePremium() builderMode := req.BuilderId != "" - aiOpts, err := getWaveAISettings(premium, builderMode) + aiOpts, err := getWaveAISettings(premium, builderMode, rtInfo) if err != nil { http.Error(w, fmt.Sprintf("WaveAI configuration error: %v", err), http.StatusInternalServerError) return diff --git a/pkg/waveobj/objrtinfo.go b/pkg/waveobj/objrtinfo.go index a09ca9c714..d484c8f63d 100644 --- a/pkg/waveobj/objrtinfo.go +++ b/pkg/waveobj/objrtinfo.go @@ -22,5 +22,7 @@ type ObjRTInfo struct { BuilderAppId string `json:"builder:appid,omitempty"` BuilderEnv map[string]string `json:"builder:env,omitempty"` - WaveAIChatId string `json:"waveai:chatid,omitempty"` + WaveAIChatId string `json:"waveai:chatid,omitempty"` + WaveAIThinkingLevel string `json:"waveai:thinkinglevel,omitempty"` + WaveAIMaxOutputTokens int `json:"waveai:maxoutputtokens,omitempty"` }