Skip to content

Commit c9c6192

Browse files
authored
minor v0.13 fixes (#2649)
1 parent 4ac5e0b commit c9c6192

24 files changed

+261
-49
lines changed

cmd/server/main-server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"time"
1515

1616
"github.com/joho/godotenv"
17+
"github.com/wavetermdev/waveterm/pkg/aiusechat"
1718
"github.com/wavetermdev/waveterm/pkg/authkey"
1819
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
1920
"github.com/wavetermdev/waveterm/pkg/blocklogger"
@@ -526,6 +527,7 @@ func main() {
526527
sigutil.InstallShutdownSignalHandlers(doShutdown)
527528
sigutil.InstallSIGUSR1Handler()
528529
startConfigWatcher()
530+
aiusechat.InitAIModeConfigWatcher()
529531
maybeStartPprofServer()
530532
go stdinReadWatch()
531533
go telemetryLoop()

frontend/app/aipanel/ai-utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,25 @@ export const getFilteredAIModeConfigs = (
572572
shouldShowCloudModes,
573573
};
574574
};
575+
576+
/**
577+
* Get the display name for an AI mode configuration.
578+
* If display:name is set, use that. Otherwise, construct from model/provider.
579+
* For azure-legacy, show "azureresourcename (azure)".
580+
* For other providers, show "model (provider)".
581+
*/
582+
export function getModeDisplayName(config: AIModeConfigType): string {
583+
if (config["display:name"]) {
584+
return config["display:name"];
585+
}
586+
587+
const provider = config["ai:provider"];
588+
const model = config["ai:model"];
589+
const azureResourceName = config["ai:azureresourcename"];
590+
591+
if (provider === "azure-legacy") {
592+
return `${azureResourceName || "unknown"} (azure)`;
593+
}
594+
595+
return `${model || "unknown"} (${provider || "custom"})`;
596+
}

frontend/app/aipanel/aimode.tsx

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { Tooltip } from "@/app/element/tooltip";
45
import { atoms, getSettingsKeyAtom } from "@/app/store/global";
56
import { RpcApi } from "@/app/store/wshclientapi";
67
import { TabRpcClient } from "@/app/store/wshrpcutil";
78
import { cn, fireAndForget, makeIconClass } from "@/util/util";
89
import { useAtomValue } from "jotai";
910
import { memo, useRef, useState } from "react";
10-
import { getFilteredAIModeConfigs } from "./ai-utils";
11+
import { getFilteredAIModeConfigs, getModeDisplayName } from "./ai-utils";
1112
import { WaveAIModel } from "./waveai-model";
1213

1314
interface AIModeMenuItemProps {
14-
config: any;
15+
config: AIModeConfigWithMode;
1516
isSelected: boolean;
1617
isDisabled: boolean;
1718
onClick: () => void;
@@ -34,13 +35,16 @@ const AIModeMenuItem = memo(({ config, isSelected, isDisabled, onClick, isFirst,
3435
<div className="flex items-center gap-2 w-full">
3536
<i className={makeIconClass(config["display:icon"] || "sparkles", false)}></i>
3637
<span className={cn("text-sm", isSelected && "font-bold")}>
37-
{config["display:name"]}
38+
{getModeDisplayName(config)}
3839
{isDisabled && " (premium)"}
3940
</span>
4041
{isSelected && <i className="fa fa-check ml-auto"></i>}
4142
</div>
4243
{config["display:description"] && (
43-
<div className={cn("text-xs pl-5", isDisabled ? "text-gray-500" : "text-muted")} style={{ whiteSpace: "pre-line" }}>
44+
<div
45+
className={cn("text-xs pl-5", isDisabled ? "text-gray-500" : "text-muted")}
46+
style={{ whiteSpace: "pre-line" }}
47+
>
4448
{config["display:description"]}
4549
</div>
4650
)}
@@ -52,26 +56,26 @@ AIModeMenuItem.displayName = "AIModeMenuItem";
5256

5357
interface ConfigSection {
5458
sectionName: string;
55-
configs: any[];
59+
configs: AIModeConfigWithMode[];
5660
isIncompatible?: boolean;
5761
}
5862

5963
function computeCompatibleSections(
6064
currentMode: string,
61-
aiModeConfigs: Record<string, any>,
62-
waveProviderConfigs: any[],
63-
otherProviderConfigs: any[]
65+
aiModeConfigs: Record<string, AIModeConfigType>,
66+
waveProviderConfigs: AIModeConfigWithMode[],
67+
otherProviderConfigs: AIModeConfigWithMode[]
6468
): ConfigSection[] {
6569
const currentConfig = aiModeConfigs[currentMode];
6670
const allConfigs = [...waveProviderConfigs, ...otherProviderConfigs];
67-
71+
6872
if (!currentConfig) {
6973
return [{ sectionName: "Incompatible Modes", configs: allConfigs, isIncompatible: true }];
7074
}
71-
75+
7276
const currentSwitchCompat = currentConfig["ai:switchcompat"] || [];
73-
const compatibleConfigs: any[] = [currentConfig];
74-
const incompatibleConfigs: any[] = [];
77+
const compatibleConfigs: AIModeConfigWithMode[] = [{ ...currentConfig, mode: currentMode }];
78+
const incompatibleConfigs: AIModeConfigWithMode[] = [];
7579

7680
if (currentSwitchCompat.length === 0) {
7781
allConfigs.forEach((config) => {
@@ -82,12 +86,10 @@ function computeCompatibleSections(
8286
} else {
8387
allConfigs.forEach((config) => {
8488
if (config.mode === currentMode) return;
85-
89+
8690
const configSwitchCompat = config["ai:switchcompat"] || [];
87-
const hasMatch = currentSwitchCompat.some((currentTag: string) =>
88-
configSwitchCompat.includes(currentTag)
89-
);
90-
91+
const hasMatch = currentSwitchCompat.some((currentTag: string) => configSwitchCompat.includes(currentTag));
92+
9193
if (hasMatch) {
9294
compatibleConfigs.push(config);
9395
} else {
@@ -99,24 +101,24 @@ function computeCompatibleSections(
99101
const sections: ConfigSection[] = [];
100102
const compatibleSectionName = compatibleConfigs.length === 1 ? "Current" : "Compatible Modes";
101103
sections.push({ sectionName: compatibleSectionName, configs: compatibleConfigs });
102-
104+
103105
if (incompatibleConfigs.length > 0) {
104106
sections.push({ sectionName: "Incompatible Modes", configs: incompatibleConfigs, isIncompatible: true });
105107
}
106108

107109
return sections;
108110
}
109111

110-
function computeWaveCloudSections(waveProviderConfigs: any[], otherProviderConfigs: any[]): ConfigSection[] {
112+
function computeWaveCloudSections(waveProviderConfigs: AIModeConfigWithMode[], otherProviderConfigs: AIModeConfigWithMode[]): ConfigSection[] {
111113
const sections: ConfigSection[] = [];
112-
114+
113115
if (waveProviderConfigs.length > 0) {
114116
sections.push({ sectionName: "Wave AI Cloud", configs: waveProviderConfigs });
115117
}
116118
if (otherProviderConfigs.length > 0) {
117119
sections.push({ sectionName: "Custom", configs: otherProviderConfigs });
118120
}
119-
121+
120122
return sections;
121123
}
122124

@@ -128,6 +130,8 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
128130
const model = WaveAIModel.getInstance();
129131
const aiMode = useAtomValue(model.currentAIMode);
130132
const aiModeConfigs = useAtomValue(model.aiModeConfigs);
133+
const waveaiModeConfigs = useAtomValue(atoms.waveaiModeConfigAtom);
134+
const widgetContextEnabled = useAtomValue(model.widgetAccessAtom);
131135
const rateLimitInfo = useAtomValue(atoms.waveAIRateLimitInfoAtom);
132136
const showCloudModes = useAtomValue(getSettingsKeyAtom("waveai:showcloudmodes"));
133137
const defaultMode = useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced";
@@ -170,10 +174,12 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
170174
setIsOpen(false);
171175
};
172176

173-
const displayConfig = aiModeConfigs[currentMode] || {
174-
"display:name": "? Unknown",
175-
"display:icon": "question",
176-
};
177+
const displayConfig = aiModeConfigs[currentMode];
178+
const displayName = displayConfig ? getModeDisplayName(displayConfig) : "Unknown";
179+
const displayIcon = displayConfig?.["display:icon"] || "sparkles";
180+
const resolvedConfig = waveaiModeConfigs[currentMode];
181+
const hasToolsSupport = resolvedConfig && resolvedConfig["ai:capabilities"]?.includes("tools");
182+
const showNoToolsWarning = widgetContextEnabled && resolvedConfig && !hasToolsSupport;
177183

178184
const handleConfigureClick = () => {
179185
fireAndForget(async () => {
@@ -200,29 +206,50 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
200206
"group flex items-center gap-1.5 px-2 py-1 text-xs text-gray-300 hover:text-white rounded transition-colors cursor-pointer border border-gray-600/50",
201207
isOpen ? "bg-gray-700" : "bg-gray-800/50 hover:bg-gray-700"
202208
)}
203-
title={`AI Mode: ${displayConfig["display:name"]}`}
209+
title={`AI Mode: ${displayName}`}
204210
>
205-
<i className={cn(makeIconClass(displayConfig["display:icon"] || "sparkles", false), "text-[10px]")}></i>
206-
<span className={`text-[11px]`}>
207-
{displayConfig["display:name"]}
208-
</span>
211+
<i className={cn(makeIconClass(displayIcon, false), "text-[10px]")}></i>
212+
<span className={`text-[11px]`}>{displayName}</span>
209213
<i className="fa fa-chevron-down text-[8px]"></i>
210214
</button>
211215

216+
{showNoToolsWarning && (
217+
<Tooltip
218+
content={
219+
<div className="max-w-xs">
220+
Warning: This custom mode was configured without the "tools" capability in the
221+
"ai:capabilities" array. Without tool support, Wave AI will not be able to interact with
222+
widgets or files.
223+
</div>
224+
}
225+
placement="bottom"
226+
>
227+
<div className="flex items-center gap-1 text-[10px] text-yellow-600 mt-1 ml-1 cursor-default">
228+
<i className="fa fa-triangle-exclamation"></i>
229+
<span>No Tools Support</span>
230+
</div>
231+
</Tooltip>
232+
)}
233+
212234
{isOpen && (
213235
<>
214236
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
215237
<div className="absolute top-full left-0 mt-1 bg-gray-800 border border-gray-600 rounded shadow-lg z-50 min-w-[280px]">
216238
{sections.map((section, sectionIndex) => {
217239
const isFirstSection = sectionIndex === 0;
218240
const isLastSection = sectionIndex === sections.length - 1;
219-
241+
220242
return (
221243
<div key={section.sectionName}>
222244
{!isFirstSection && <div className="border-t border-gray-600 my-2" />}
223245
{showSectionHeaders && (
224246
<>
225-
<div className={cn("pb-1 text-center text-[10px] text-gray-400 uppercase tracking-wide", isFirstSection ? "pt-2" : "pt-0")}>
247+
<div
248+
className={cn(
249+
"pb-1 text-center text-[10px] text-gray-400 uppercase tracking-wide",
250+
isFirstSection ? "pt-2" : "pt-0"
251+
)}
252+
>
226253
{section.sectionName}
227254
</div>
228255
{section.isIncompatible && (

frontend/app/aipanel/aipanel-contextmenu.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { getFilteredAIModeConfigs } from "@/app/aipanel/ai-utils";
4+
import { getFilteredAIModeConfigs, getModeDisplayName } from "@/app/aipanel/ai-utils";
55
import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils";
66
import { ContextMenuModel } from "@/app/store/contextmenu";
77
import { atoms, getSettingsKeyAtom, isDev } from "@/app/store/global";
@@ -68,7 +68,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
6868
const isPremium = config["waveai:premium"] === true;
6969
const isEnabled = !isPremium || hasPremium;
7070
aiModeSubmenu.push({
71-
label: config["display:name"] || mode,
71+
label: getModeDisplayName(config),
7272
type: "checkbox",
7373
checked: currentAIMode === mode,
7474
enabled: isEnabled,
@@ -98,7 +98,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
9898
const isPremium = config["waveai:premium"] === true;
9999
const isEnabled = !isPremium || hasPremium;
100100
aiModeSubmenu.push({
101-
label: config["display:name"] || mode,
101+
label: getModeDisplayName(config),
102102
type: "checkbox",
103103
checked: currentAIMode === mode,
104104
enabled: isEnabled,
@@ -201,6 +201,25 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
201201
submenu: maxTokensSubmenu,
202202
});
203203

204+
menu.push({ type: "separator" });
205+
206+
menu.push({
207+
label: "Configure Modes",
208+
click: () => {
209+
RpcApi.RecordTEventCommand(
210+
TabRpcClient,
211+
{
212+
event: "action:other",
213+
props: {
214+
"action:type": "waveai:configuremodes:contextmenu",
215+
},
216+
},
217+
{ noresponse: true }
218+
);
219+
model.openWaveAIConfig();
220+
},
221+
});
222+
204223
if (model.canCloseWaveAIPanel()) {
205224
menu.push({ type: "separator" });
206225

frontend/app/aipanel/aipanel.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { AIPanelHeader } from "./aipanelheader";
2121
import { AIPanelInput } from "./aipanelinput";
2222
import { AIPanelMessages } from "./aipanelmessages";
2323
import { AIRateLimitStrip } from "./airatelimitstrip";
24+
import { WaveUIMessage } from "./aitypes";
2425
import { BYOKAnnouncement } from "./byokannouncement";
2526
import { TelemetryRequiredMessage } from "./telemetryrequired";
2627
import { WaveAIModel } from "./waveai-model";
@@ -83,6 +84,10 @@ KeyCap.displayName = "KeyCap";
8384

8485
const AIWelcomeMessage = memo(() => {
8586
const modKey = isMacOS() ? "⌘" : "Alt";
87+
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
88+
const hasCustomModes = fullConfig?.waveai
89+
? Object.keys(fullConfig.waveai).some((key) => !key.startsWith("waveai@"))
90+
: false;
8691
return (
8792
<div className="text-secondary py-8">
8893
<div className="text-center">
@@ -155,7 +160,7 @@ const AIWelcomeMessage = memo(() => {
155160
</div>
156161
</div>
157162
</div>
158-
<BYOKAnnouncement />
163+
{!hasCustomModes && <BYOKAnnouncement />}
159164
<div className="mt-4 text-center text-[12px] text-muted">
160165
BETA: Free to use. Daily limits keep our costs in check.
161166
</div>
@@ -219,7 +224,7 @@ const AIPanelComponentInner = memo(() => {
219224
const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
220225
const isPanelVisible = jotai.useAtomValue(model.getPanelVisibleAtom());
221226

222-
const { messages, sendMessage, status, setMessages, error, stop } = useChat({
227+
const { messages, sendMessage, status, setMessages, error, stop } = useChat<WaveUIMessage>({
223228
transport: new DefaultChatTransport({
224229
api: model.getUseChatEndpointUrl(),
225230
prepareSendMessagesRequest: (opts) => {

frontend/app/aipanel/aipanelheader.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ export const AIPanelHeader = memo(() => {
1515
handleWaveAIContextMenu(e, false);
1616
};
1717

18+
const handleContextMenu = (e: React.MouseEvent) => {
19+
handleWaveAIContextMenu(e, false);
20+
};
21+
1822
return (
19-
<div className="py-2 pl-3 pr-1 @xs:p-2 @xs:pl-4 border-b border-gray-600 flex items-center justify-between min-w-0">
23+
<div
24+
className="py-2 pl-3 pr-1 @xs:p-2 @xs:pl-4 border-b border-gray-600 flex items-center justify-between min-w-0"
25+
onContextMenu={handleContextMenu}
26+
>
2027
<h2 className="text-white text-sm @xs:text-lg font-semibold flex items-center gap-2 flex-shrink-0 whitespace-nowrap">
2128
<i className="fa fa-sparkles text-accent"></i>
2229
Wave AI

frontend/app/aipanel/aipanelmessages.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { useAtomValue } from "jotai";
55
import { memo, useEffect, useRef } from "react";
66
import { AIMessage } from "./aimessage";
77
import { AIModeDropdown } from "./aimode";
8+
import { type WaveUIMessage } from "./aitypes";
89
import { WaveAIModel } from "./waveai-model";
910

1011
interface AIPanelMessagesProps {
11-
messages: any[];
12+
messages: WaveUIMessage[];
1213
status: string;
1314
onContextMenu?: (e: React.MouseEvent) => void;
1415
}

frontend/app/aipanel/byokannouncement.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const BYOKAnnouncement = () => {
3636
};
3737

3838
return (
39-
<div className="bg-blue-900/20 border border-blue-500 rounded-lg p-4 mt-4">
39+
<div className="bg-blue-900/20 border border-blue-800 rounded-lg p-4 mt-4">
4040
<div className="flex items-start gap-3">
4141
<i className="fa fa-key text-blue-400 text-lg mt-0.5"></i>
4242
<div className="text-left flex-1">
@@ -48,7 +48,7 @@ const BYOKAnnouncement = () => {
4848
<div className="flex items-center gap-3">
4949
<button
5050
onClick={handleOpenConfig}
51-
className="bg-blue-500/80 hover:bg-blue-500 text-secondary hover:text-primary px-3 py-1.5 rounded-md text-sm font-medium cursor-pointer transition-colors"
51+
className="border border-blue-400 text-blue-400 hover:bg-blue-500/10 hover:text-blue-300 px-3 py-1.5 rounded-md text-sm font-medium cursor-pointer transition-colors"
5252
>
5353
Configure AI Modes
5454
</button>
@@ -57,7 +57,7 @@ const BYOKAnnouncement = () => {
5757
target="_blank"
5858
rel="noopener noreferrer"
5959
onClick={handleViewDocs}
60-
className="text-secondary hover:text-primary text-sm cursor-pointer transition-colors flex items-center gap-1"
60+
className="text-blue-400! hover:text-blue-300! hover:underline text-sm cursor-pointer transition-colors flex items-center gap-1"
6161
>
6262
View Docs <i className="fa fa-external-link text-xs"></i>
6363
</a>
@@ -70,4 +70,4 @@ const BYOKAnnouncement = () => {
7070

7171
BYOKAnnouncement.displayName = "BYOKAnnouncement";
7272

73-
export { BYOKAnnouncement };
73+
export { BYOKAnnouncement };

0 commit comments

Comments
 (0)