mirror of
https://github.com/wavetermdev/waveterm
synced 2026-04-21 14:37:16 +00:00
minor v0.13 fixes (#2649)
This commit is contained in:
parent
4ac5e0b332
commit
c9c6192fc9
24 changed files with 261 additions and 49 deletions
|
|
@ -14,6 +14,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/wavetermdev/waveterm/pkg/aiusechat"
|
||||
"github.com/wavetermdev/waveterm/pkg/authkey"
|
||||
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/blocklogger"
|
||||
|
|
@ -526,6 +527,7 @@ func main() {
|
|||
sigutil.InstallShutdownSignalHandlers(doShutdown)
|
||||
sigutil.InstallSIGUSR1Handler()
|
||||
startConfigWatcher()
|
||||
aiusechat.InitAIModeConfigWatcher()
|
||||
maybeStartPprofServer()
|
||||
go stdinReadWatch()
|
||||
go telemetryLoop()
|
||||
|
|
|
|||
|
|
@ -572,3 +572,25 @@ export const getFilteredAIModeConfigs = (
|
|||
shouldShowCloudModes,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the display name for an AI mode configuration.
|
||||
* If display:name is set, use that. Otherwise, construct from model/provider.
|
||||
* For azure-legacy, show "azureresourcename (azure)".
|
||||
* For other providers, show "model (provider)".
|
||||
*/
|
||||
export function getModeDisplayName(config: AIModeConfigType): string {
|
||||
if (config["display:name"]) {
|
||||
return config["display:name"];
|
||||
}
|
||||
|
||||
const provider = config["ai:provider"];
|
||||
const model = config["ai:model"];
|
||||
const azureResourceName = config["ai:azureresourcename"];
|
||||
|
||||
if (provider === "azure-legacy") {
|
||||
return `${azureResourceName || "unknown"} (azure)`;
|
||||
}
|
||||
|
||||
return `${model || "unknown"} (${provider || "custom"})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Tooltip } from "@/app/element/tooltip";
|
||||
import { atoms, getSettingsKeyAtom } from "@/app/store/global";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { cn, fireAndForget, makeIconClass } from "@/util/util";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo, useRef, useState } from "react";
|
||||
import { getFilteredAIModeConfigs } from "./ai-utils";
|
||||
import { getFilteredAIModeConfigs, getModeDisplayName } from "./ai-utils";
|
||||
import { WaveAIModel } from "./waveai-model";
|
||||
|
||||
interface AIModeMenuItemProps {
|
||||
config: any;
|
||||
config: AIModeConfigWithMode;
|
||||
isSelected: boolean;
|
||||
isDisabled: boolean;
|
||||
onClick: () => void;
|
||||
|
|
@ -34,13 +35,16 @@ const AIModeMenuItem = memo(({ config, isSelected, isDisabled, onClick, isFirst,
|
|||
<div className="flex items-center gap-2 w-full">
|
||||
<i className={makeIconClass(config["display:icon"] || "sparkles", false)}></i>
|
||||
<span className={cn("text-sm", isSelected && "font-bold")}>
|
||||
{config["display:name"]}
|
||||
{getModeDisplayName(config)}
|
||||
{isDisabled && " (premium)"}
|
||||
</span>
|
||||
{isSelected && <i className="fa fa-check ml-auto"></i>}
|
||||
</div>
|
||||
{config["display:description"] && (
|
||||
<div className={cn("text-xs pl-5", isDisabled ? "text-gray-500" : "text-muted")} style={{ whiteSpace: "pre-line" }}>
|
||||
<div
|
||||
className={cn("text-xs pl-5", isDisabled ? "text-gray-500" : "text-muted")}
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
>
|
||||
{config["display:description"]}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -52,26 +56,26 @@ AIModeMenuItem.displayName = "AIModeMenuItem";
|
|||
|
||||
interface ConfigSection {
|
||||
sectionName: string;
|
||||
configs: any[];
|
||||
configs: AIModeConfigWithMode[];
|
||||
isIncompatible?: boolean;
|
||||
}
|
||||
|
||||
function computeCompatibleSections(
|
||||
currentMode: string,
|
||||
aiModeConfigs: Record<string, any>,
|
||||
waveProviderConfigs: any[],
|
||||
otherProviderConfigs: any[]
|
||||
aiModeConfigs: Record<string, AIModeConfigType>,
|
||||
waveProviderConfigs: AIModeConfigWithMode[],
|
||||
otherProviderConfigs: AIModeConfigWithMode[]
|
||||
): ConfigSection[] {
|
||||
const currentConfig = aiModeConfigs[currentMode];
|
||||
const allConfigs = [...waveProviderConfigs, ...otherProviderConfigs];
|
||||
|
||||
|
||||
if (!currentConfig) {
|
||||
return [{ sectionName: "Incompatible Modes", configs: allConfigs, isIncompatible: true }];
|
||||
}
|
||||
|
||||
|
||||
const currentSwitchCompat = currentConfig["ai:switchcompat"] || [];
|
||||
const compatibleConfigs: any[] = [currentConfig];
|
||||
const incompatibleConfigs: any[] = [];
|
||||
const compatibleConfigs: AIModeConfigWithMode[] = [{ ...currentConfig, mode: currentMode }];
|
||||
const incompatibleConfigs: AIModeConfigWithMode[] = [];
|
||||
|
||||
if (currentSwitchCompat.length === 0) {
|
||||
allConfigs.forEach((config) => {
|
||||
|
|
@ -82,12 +86,10 @@ function computeCompatibleSections(
|
|||
} else {
|
||||
allConfigs.forEach((config) => {
|
||||
if (config.mode === currentMode) return;
|
||||
|
||||
|
||||
const configSwitchCompat = config["ai:switchcompat"] || [];
|
||||
const hasMatch = currentSwitchCompat.some((currentTag: string) =>
|
||||
configSwitchCompat.includes(currentTag)
|
||||
);
|
||||
|
||||
const hasMatch = currentSwitchCompat.some((currentTag: string) => configSwitchCompat.includes(currentTag));
|
||||
|
||||
if (hasMatch) {
|
||||
compatibleConfigs.push(config);
|
||||
} else {
|
||||
|
|
@ -99,7 +101,7 @@ function computeCompatibleSections(
|
|||
const sections: ConfigSection[] = [];
|
||||
const compatibleSectionName = compatibleConfigs.length === 1 ? "Current" : "Compatible Modes";
|
||||
sections.push({ sectionName: compatibleSectionName, configs: compatibleConfigs });
|
||||
|
||||
|
||||
if (incompatibleConfigs.length > 0) {
|
||||
sections.push({ sectionName: "Incompatible Modes", configs: incompatibleConfigs, isIncompatible: true });
|
||||
}
|
||||
|
|
@ -107,16 +109,16 @@ function computeCompatibleSections(
|
|||
return sections;
|
||||
}
|
||||
|
||||
function computeWaveCloudSections(waveProviderConfigs: any[], otherProviderConfigs: any[]): ConfigSection[] {
|
||||
function computeWaveCloudSections(waveProviderConfigs: AIModeConfigWithMode[], otherProviderConfigs: AIModeConfigWithMode[]): ConfigSection[] {
|
||||
const sections: ConfigSection[] = [];
|
||||
|
||||
|
||||
if (waveProviderConfigs.length > 0) {
|
||||
sections.push({ sectionName: "Wave AI Cloud", configs: waveProviderConfigs });
|
||||
}
|
||||
if (otherProviderConfigs.length > 0) {
|
||||
sections.push({ sectionName: "Custom", configs: otherProviderConfigs });
|
||||
}
|
||||
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +130,8 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
|
|||
const model = WaveAIModel.getInstance();
|
||||
const aiMode = useAtomValue(model.currentAIMode);
|
||||
const aiModeConfigs = useAtomValue(model.aiModeConfigs);
|
||||
const waveaiModeConfigs = useAtomValue(atoms.waveaiModeConfigAtom);
|
||||
const widgetContextEnabled = useAtomValue(model.widgetAccessAtom);
|
||||
const rateLimitInfo = useAtomValue(atoms.waveAIRateLimitInfoAtom);
|
||||
const showCloudModes = useAtomValue(getSettingsKeyAtom("waveai:showcloudmodes"));
|
||||
const defaultMode = useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced";
|
||||
|
|
@ -170,10 +174,12 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
|
|||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const displayConfig = aiModeConfigs[currentMode] || {
|
||||
"display:name": "? Unknown",
|
||||
"display:icon": "question",
|
||||
};
|
||||
const displayConfig = aiModeConfigs[currentMode];
|
||||
const displayName = displayConfig ? getModeDisplayName(displayConfig) : "Unknown";
|
||||
const displayIcon = displayConfig?.["display:icon"] || "sparkles";
|
||||
const resolvedConfig = waveaiModeConfigs[currentMode];
|
||||
const hasToolsSupport = resolvedConfig && resolvedConfig["ai:capabilities"]?.includes("tools");
|
||||
const showNoToolsWarning = widgetContextEnabled && resolvedConfig && !hasToolsSupport;
|
||||
|
||||
const handleConfigureClick = () => {
|
||||
fireAndForget(async () => {
|
||||
|
|
@ -200,15 +206,31 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
|
|||
"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",
|
||||
isOpen ? "bg-gray-700" : "bg-gray-800/50 hover:bg-gray-700"
|
||||
)}
|
||||
title={`AI Mode: ${displayConfig["display:name"]}`}
|
||||
title={`AI Mode: ${displayName}`}
|
||||
>
|
||||
<i className={cn(makeIconClass(displayConfig["display:icon"] || "sparkles", false), "text-[10px]")}></i>
|
||||
<span className={`text-[11px]`}>
|
||||
{displayConfig["display:name"]}
|
||||
</span>
|
||||
<i className={cn(makeIconClass(displayIcon, false), "text-[10px]")}></i>
|
||||
<span className={`text-[11px]`}>{displayName}</span>
|
||||
<i className="fa fa-chevron-down text-[8px]"></i>
|
||||
</button>
|
||||
|
||||
{showNoToolsWarning && (
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="max-w-xs">
|
||||
Warning: This custom mode was configured without the "tools" capability in the
|
||||
"ai:capabilities" array. Without tool support, Wave AI will not be able to interact with
|
||||
widgets or files.
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="flex items-center gap-1 text-[10px] text-yellow-600 mt-1 ml-1 cursor-default">
|
||||
<i className="fa fa-triangle-exclamation"></i>
|
||||
<span>No Tools Support</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
||||
|
|
@ -216,13 +238,18 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
|
|||
{sections.map((section, sectionIndex) => {
|
||||
const isFirstSection = sectionIndex === 0;
|
||||
const isLastSection = sectionIndex === sections.length - 1;
|
||||
|
||||
|
||||
return (
|
||||
<div key={section.sectionName}>
|
||||
{!isFirstSection && <div className="border-t border-gray-600 my-2" />}
|
||||
{showSectionHeaders && (
|
||||
<>
|
||||
<div className={cn("pb-1 text-center text-[10px] text-gray-400 uppercase tracking-wide", isFirstSection ? "pt-2" : "pt-0")}>
|
||||
<div
|
||||
className={cn(
|
||||
"pb-1 text-center text-[10px] text-gray-400 uppercase tracking-wide",
|
||||
isFirstSection ? "pt-2" : "pt-0"
|
||||
)}
|
||||
>
|
||||
{section.sectionName}
|
||||
</div>
|
||||
{section.isIncompatible && (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { getFilteredAIModeConfigs } from "@/app/aipanel/ai-utils";
|
||||
import { getFilteredAIModeConfigs, getModeDisplayName } from "@/app/aipanel/ai-utils";
|
||||
import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { atoms, getSettingsKeyAtom, isDev } from "@/app/store/global";
|
||||
|
|
@ -68,7 +68,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
|
|||
const isPremium = config["waveai:premium"] === true;
|
||||
const isEnabled = !isPremium || hasPremium;
|
||||
aiModeSubmenu.push({
|
||||
label: config["display:name"] || mode,
|
||||
label: getModeDisplayName(config),
|
||||
type: "checkbox",
|
||||
checked: currentAIMode === mode,
|
||||
enabled: isEnabled,
|
||||
|
|
@ -98,7 +98,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
|
|||
const isPremium = config["waveai:premium"] === true;
|
||||
const isEnabled = !isPremium || hasPremium;
|
||||
aiModeSubmenu.push({
|
||||
label: config["display:name"] || mode,
|
||||
label: getModeDisplayName(config),
|
||||
type: "checkbox",
|
||||
checked: currentAIMode === mode,
|
||||
enabled: isEnabled,
|
||||
|
|
@ -201,6 +201,25 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
|
|||
submenu: maxTokensSubmenu,
|
||||
});
|
||||
|
||||
menu.push({ type: "separator" });
|
||||
|
||||
menu.push({
|
||||
label: "Configure Modes",
|
||||
click: () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:other",
|
||||
props: {
|
||||
"action:type": "waveai:configuremodes:contextmenu",
|
||||
},
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
model.openWaveAIConfig();
|
||||
},
|
||||
});
|
||||
|
||||
if (model.canCloseWaveAIPanel()) {
|
||||
menu.push({ type: "separator" });
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { AIPanelHeader } from "./aipanelheader";
|
|||
import { AIPanelInput } from "./aipanelinput";
|
||||
import { AIPanelMessages } from "./aipanelmessages";
|
||||
import { AIRateLimitStrip } from "./airatelimitstrip";
|
||||
import { WaveUIMessage } from "./aitypes";
|
||||
import { BYOKAnnouncement } from "./byokannouncement";
|
||||
import { TelemetryRequiredMessage } from "./telemetryrequired";
|
||||
import { WaveAIModel } from "./waveai-model";
|
||||
|
|
@ -83,6 +84,10 @@ KeyCap.displayName = "KeyCap";
|
|||
|
||||
const AIWelcomeMessage = memo(() => {
|
||||
const modKey = isMacOS() ? "⌘" : "Alt";
|
||||
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
|
||||
const hasCustomModes = fullConfig?.waveai
|
||||
? Object.keys(fullConfig.waveai).some((key) => !key.startsWith("waveai@"))
|
||||
: false;
|
||||
return (
|
||||
<div className="text-secondary py-8">
|
||||
<div className="text-center">
|
||||
|
|
@ -155,7 +160,7 @@ const AIWelcomeMessage = memo(() => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BYOKAnnouncement />
|
||||
{!hasCustomModes && <BYOKAnnouncement />}
|
||||
<div className="mt-4 text-center text-[12px] text-muted">
|
||||
BETA: Free to use. Daily limits keep our costs in check.
|
||||
</div>
|
||||
|
|
@ -219,7 +224,7 @@ const AIPanelComponentInner = memo(() => {
|
|||
const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
|
||||
const isPanelVisible = jotai.useAtomValue(model.getPanelVisibleAtom());
|
||||
|
||||
const { messages, sendMessage, status, setMessages, error, stop } = useChat({
|
||||
const { messages, sendMessage, status, setMessages, error, stop } = useChat<WaveUIMessage>({
|
||||
transport: new DefaultChatTransport({
|
||||
api: model.getUseChatEndpointUrl(),
|
||||
prepareSendMessagesRequest: (opts) => {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,15 @@ export const AIPanelHeader = memo(() => {
|
|||
handleWaveAIContextMenu(e, false);
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
handleWaveAIContextMenu(e, false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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"
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<h2 className="text-white text-sm @xs:text-lg font-semibold flex items-center gap-2 flex-shrink-0 whitespace-nowrap">
|
||||
<i className="fa fa-sparkles text-accent"></i>
|
||||
Wave AI
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import { useAtomValue } from "jotai";
|
|||
import { memo, useEffect, useRef } from "react";
|
||||
import { AIMessage } from "./aimessage";
|
||||
import { AIModeDropdown } from "./aimode";
|
||||
import { type WaveUIMessage } from "./aitypes";
|
||||
import { WaveAIModel } from "./waveai-model";
|
||||
|
||||
interface AIPanelMessagesProps {
|
||||
messages: any[];
|
||||
messages: WaveUIMessage[];
|
||||
status: string;
|
||||
onContextMenu?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const BYOKAnnouncement = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="bg-blue-900/20 border border-blue-500 rounded-lg p-4 mt-4">
|
||||
<div className="bg-blue-900/20 border border-blue-800 rounded-lg p-4 mt-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<i className="fa fa-key text-blue-400 text-lg mt-0.5"></i>
|
||||
<div className="text-left flex-1">
|
||||
|
|
@ -48,7 +48,7 @@ const BYOKAnnouncement = () => {
|
|||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleOpenConfig}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
Configure AI Modes
|
||||
</button>
|
||||
|
|
@ -57,7 +57,7 @@ const BYOKAnnouncement = () => {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleViewDocs}
|
||||
className="text-secondary hover:text-primary text-sm cursor-pointer transition-colors flex items-center gap-1"
|
||||
className="text-blue-400! hover:text-blue-300! hover:underline text-sm cursor-pointer transition-colors flex items-center gap-1"
|
||||
>
|
||||
View Docs <i className="fa fa-external-link text-xs"></i>
|
||||
</a>
|
||||
|
|
@ -70,4 +70,4 @@ const BYOKAnnouncement = () => {
|
|||
|
||||
BYOKAnnouncement.displayName = "BYOKAnnouncement";
|
||||
|
||||
export { BYOKAnnouncement };
|
||||
export { BYOKAnnouncement };
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
|||
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
|
||||
});
|
||||
const fullConfigAtom = atom(null) as PrimitiveAtom<FullConfigType>;
|
||||
const waveaiModeConfigAtom = atom(null) as PrimitiveAtom<Record<string, AIModeConfigType>>;
|
||||
const settingsAtom = atom((get) => {
|
||||
return get(fullConfigAtom)?.settings ?? {};
|
||||
}) as Atom<SettingsType>;
|
||||
|
|
@ -180,6 +181,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
|||
waveWindow: windowDataAtom,
|
||||
workspace: workspaceAtom,
|
||||
fullConfigAtom,
|
||||
waveaiModeConfigAtom,
|
||||
settingsAtom,
|
||||
hasCustomAIPresetsAtom,
|
||||
tabAtom,
|
||||
|
|
@ -218,6 +220,13 @@ function initGlobalWaveEventSubs(initOpts: WaveInitOpts) {
|
|||
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
||||
},
|
||||
},
|
||||
{
|
||||
eventType: "waveai:modeconfig",
|
||||
handler: (event) => {
|
||||
const modeConfigs = (event.data as AIModeConfigUpdate).configs;
|
||||
globalStore.set(atoms.waveaiModeConfigAtom, modeConfigs);
|
||||
},
|
||||
},
|
||||
{
|
||||
eventType: "userinput",
|
||||
handler: (event) => {
|
||||
|
|
|
|||
|
|
@ -352,6 +352,11 @@ class RpcApiType {
|
|||
return client.wshRpcCall("getwaveaichat", data, opts);
|
||||
}
|
||||
|
||||
// command "getwaveaimodeconfig" [call]
|
||||
GetWaveAIModeConfigCommand(client: WshClient, opts?: RpcOpts): Promise<AIModeConfigUpdate> {
|
||||
return client.wshRpcCall("getwaveaimodeconfig", null, opts);
|
||||
}
|
||||
|
||||
// command "getwaveairatelimit" [call]
|
||||
GetWaveAIRateLimitCommand(client: WshClient, opts?: RpcOpts): Promise<RateLimitInfo> {
|
||||
return client.wshRpcCall("getwaveairatelimit", null, opts);
|
||||
|
|
|
|||
3
frontend/types/custom.d.ts
vendored
3
frontend/types/custom.d.ts
vendored
|
|
@ -16,6 +16,7 @@ declare global {
|
|||
waveWindow: jotai.Atom<WaveWindow>; // driven from WOS
|
||||
workspace: jotai.Atom<Workspace>; // driven from WOS
|
||||
fullConfigAtom: jotai.PrimitiveAtom<FullConfigType>; // driven from WOS, settings -- updated via WebSocket
|
||||
waveaiModeConfigAtom: jotai.PrimitiveAtom<Record<string, AIModeConfigType>>; // resolved AI mode configs -- updated via WebSocket
|
||||
settingsAtom: jotai.Atom<SettingsType>; // derrived from fullConfig
|
||||
hasCustomAIPresetsAtom: jotai.Atom<boolean>; // derived from fullConfig
|
||||
tabAtom: jotai.Atom<Tab>; // driven from WOS
|
||||
|
|
@ -496,6 +497,8 @@ declare global {
|
|||
size?: number;
|
||||
previewurl?: string;
|
||||
};
|
||||
|
||||
type AIModeConfigWithMode = { mode: string } & AIModeConfigType;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
|||
9
frontend/types/gotypes.d.ts
vendored
9
frontend/types/gotypes.d.ts
vendored
|
|
@ -35,6 +35,11 @@ declare global {
|
|||
"waveai:premium"?: boolean;
|
||||
};
|
||||
|
||||
// wconfig.AIModeConfigUpdate
|
||||
type AIModeConfigUpdate = {
|
||||
configs: {[key: string]: AIModeConfigType};
|
||||
};
|
||||
|
||||
// wshrpc.ActivityDisplayType
|
||||
type ActivityDisplayType = {
|
||||
width: number;
|
||||
|
|
@ -1214,6 +1219,8 @@ declare global {
|
|||
"settings:customwidgets"?: number;
|
||||
"settings:customaipresets"?: number;
|
||||
"settings:customsettings"?: number;
|
||||
"settings:customaimodes"?: number;
|
||||
"settings:secretscount"?: number;
|
||||
"activity:activeminutes"?: number;
|
||||
"activity:fgminutes"?: number;
|
||||
"activity:openminutes"?: number;
|
||||
|
|
@ -1296,6 +1303,8 @@ declare global {
|
|||
"settings:customwidgets"?: number;
|
||||
"settings:customaipresets"?: number;
|
||||
"settings:customsettings"?: number;
|
||||
"settings:customaimodes"?: number;
|
||||
"settings:secretscount"?: number;
|
||||
};
|
||||
|
||||
// waveobj.Tab
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@ async function initWave(initOpts: WaveInitOpts) {
|
|||
const fullConfig = await RpcApi.GetFullConfigCommand(TabRpcClient);
|
||||
console.log("fullconfig", fullConfig);
|
||||
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
||||
const waveaiModeConfig = await RpcApi.GetWaveAIModeConfigCommand(TabRpcClient);
|
||||
globalStore.set(atoms.waveaiModeConfigAtom, waveaiModeConfig.configs);
|
||||
console.log("Wave First Render");
|
||||
let firstRenderResolveFn: () => void = null;
|
||||
let firstRenderPromise = new Promise<void>((resolve) => {
|
||||
|
|
@ -283,6 +285,8 @@ async function initBuilder(initOpts: BuilderInitOpts) {
|
|||
const fullConfig = await RpcApi.GetFullConfigCommand(TabRpcClient);
|
||||
console.log("fullconfig", fullConfig);
|
||||
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
||||
const waveaiModeConfig = await RpcApi.GetWaveAIModeConfigCommand(TabRpcClient);
|
||||
globalStore.set(atoms.waveaiModeConfigAtom, waveaiModeConfig.configs);
|
||||
|
||||
console.log("Tsunami Builder First Render");
|
||||
let firstRenderResolveFn: () => void = null;
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "waveterm",
|
||||
"version": "0.13.0-beta.0",
|
||||
"version": "0.13.0-beta.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "waveterm",
|
||||
"version": "0.13.0-beta.0",
|
||||
"version": "0.13.0-beta.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ package aiusechat
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/aiusechat/aiutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
)
|
||||
|
||||
var AzureResourceNameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
|
||||
|
|
@ -236,3 +238,37 @@ func getAIModeConfig(aiMode string) (*wconfig.AIModeConfigType, error) {
|
|||
applyProviderDefaults(&config)
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func InitAIModeConfigWatcher() {
|
||||
watcher := wconfig.GetWatcher()
|
||||
watcher.RegisterUpdateHandler(handleConfigUpdate)
|
||||
log.Printf("AI mode config watcher initialized\n")
|
||||
}
|
||||
|
||||
func handleConfigUpdate(fullConfig wconfig.FullConfigType) {
|
||||
resolvedConfigs := ComputeResolvedAIModeConfigs(fullConfig)
|
||||
broadcastAIModeConfigs(resolvedConfigs)
|
||||
}
|
||||
|
||||
func ComputeResolvedAIModeConfigs(fullConfig wconfig.FullConfigType) map[string]wconfig.AIModeConfigType {
|
||||
resolvedConfigs := make(map[string]wconfig.AIModeConfigType)
|
||||
|
||||
for modeName, modeConfig := range fullConfig.WaveAIModes {
|
||||
resolved := modeConfig
|
||||
applyProviderDefaults(&resolved)
|
||||
resolvedConfigs[modeName] = resolved
|
||||
}
|
||||
|
||||
return resolvedConfigs
|
||||
}
|
||||
|
||||
func broadcastAIModeConfigs(configs map[string]wconfig.AIModeConfigType) {
|
||||
update := wconfig.AIModeConfigUpdate{
|
||||
Configs: configs,
|
||||
}
|
||||
|
||||
wps.Broker.Publish(wps.WaveEvent{
|
||||
Event: wps.Event_AIModeConfig,
|
||||
Data: update,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,43 @@ var SystemPromptText_OpenAI = strings.Join([]string{
|
|||
`You have NO API access to widgets or Wave unless provided via an explicit tool.`,
|
||||
}, " ")
|
||||
|
||||
var SystemPromptText_NoTools = strings.Join([]string{
|
||||
`You are Wave AI, an assistant embedded in Wave Terminal (a terminal with graphical widgets).`,
|
||||
`You appear as a pull-out panel on the left; widgets are on the right.`,
|
||||
|
||||
// Capabilities & truthfulness
|
||||
`Be truthful about your capabilities. You can answer questions, explain concepts, provide code examples, and help with technical problems, but you cannot directly access files, execute commands, or interact with the terminal. If you lack specific data or access, say so directly and suggest what the user could do to provide it.`,
|
||||
|
||||
// Crisp behavior
|
||||
`Be concise and direct. Prefer determinism over speculation. If a brief clarifying question eliminates guesswork, ask it.`,
|
||||
|
||||
// Attached text files
|
||||
`User-attached text files may appear inline as <AttachedTextFile_xxxxxxxx file_name="...">\ncontent\n</AttachedTextFile_xxxxxxxx>.`,
|
||||
`User-attached directories use the tag <AttachedDirectoryListing_xxxxxxxx directory_name="...">JSON DirInfo</AttachedDirectoryListing_xxxxxxxx>.`,
|
||||
`If multiple attached files exist, treat each as a separate source file with its own file_name.`,
|
||||
`When the user refers to these files, use their inline content directly for analysis and discussion.`,
|
||||
|
||||
// Output & formatting
|
||||
`When presenting commands or any runnable multi-line code, always use fenced Markdown code blocks.`,
|
||||
`Use an appropriate language hint after the opening fence (e.g., "bash" for shell commands, "go" for Go, "json" for JSON).`,
|
||||
`For shell commands, do NOT prefix lines with "$" or shell prompts. Use placeholders in ALL_CAPS (e.g., PROJECT_ID) and explain them once after the block if needed.`,
|
||||
"Reserve inline code (single backticks) for short references like command names (`grep`, `less`), flags, env vars, file paths, or tiny snippets not meant to be executed.",
|
||||
`You may use Markdown (lists, tables, bold/italics) to improve readability.`,
|
||||
`Never comment on or justify your formatting choices; just follow these rules.`,
|
||||
`When generating code or command blocks, try to keep lines under ~100 characters wide where practical (soft wrap; do not break tokens mid-word). Favor indentation and short variable names to stay compact, but correctness always takes priority.`,
|
||||
|
||||
// Safety & limits
|
||||
`If a request would execute dangerous or destructive actions, warn briefly and provide a safer alternative.`,
|
||||
`If output is very long, prefer a brief summary plus a copy-ready fenced block or offer a follow-up chunking strategy.`,
|
||||
|
||||
`You cannot directly write files, execute shell commands, run code in the terminal, or access remote files.`,
|
||||
`When users ask for code or commands, provide ready-to-use examples they can copy and execute themselves.`,
|
||||
`If they need file modifications, show the exact changes they should make.`,
|
||||
|
||||
// Final reminder
|
||||
`You have NO API access to widgets or Wave Terminal internals.`,
|
||||
}, " ")
|
||||
|
||||
var SystemPromptText_StrictToolAddOn = `## Tool Call Rules (STRICT)
|
||||
|
||||
When you decide a file write/edit tool call is needed:
|
||||
|
|
|
|||
|
|
@ -47,14 +47,18 @@ var (
|
|||
activeChats = ds.MakeSyncMap[bool]() // key is chatid
|
||||
)
|
||||
|
||||
func getSystemPrompt(apiType string, model string, isBuilder bool) []string {
|
||||
func getSystemPrompt(apiType string, model string, isBuilder bool, hasToolsCapability bool, widgetAccess bool) []string {
|
||||
if isBuilder {
|
||||
return []string{}
|
||||
}
|
||||
useNoToolsPrompt := !hasToolsCapability || !widgetAccess
|
||||
basePrompt := SystemPromptText_OpenAI
|
||||
if useNoToolsPrompt {
|
||||
basePrompt = SystemPromptText_NoTools
|
||||
}
|
||||
modelLower := strings.ToLower(model)
|
||||
needsStrictToolAddOn, _ := regexp.MatchString(`(?i)\b(mistral|o?llama|qwen|mixtral|yi|phi|deepseek)\b`, modelLower)
|
||||
if needsStrictToolAddOn {
|
||||
if needsStrictToolAddOn && !useNoToolsPrompt {
|
||||
return []string{basePrompt, SystemPromptText_StrictToolAddOn}
|
||||
}
|
||||
return []string{basePrompt}
|
||||
|
|
@ -86,6 +90,7 @@ func getWaveAISettings(premium bool, builderMode bool, rtInfo waveobj.ObjRTInfo)
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve secret %s: %w", config.APITokenSecretName, err)
|
||||
}
|
||||
secret = strings.TrimSpace(secret)
|
||||
if !exists || secret == "" {
|
||||
return nil, fmt.Errorf("secret %s not found or empty", config.APITokenSecretName)
|
||||
}
|
||||
|
|
@ -658,7 +663,7 @@ func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
BuilderId: req.BuilderId,
|
||||
BuilderAppId: req.BuilderAppId,
|
||||
}
|
||||
chatOpts.SystemPrompt = getSystemPrompt(chatOpts.Config.APIType, chatOpts.Config.Model, chatOpts.BuilderId != "")
|
||||
chatOpts.SystemPrompt = getSystemPrompt(chatOpts.Config.APIType, chatOpts.Config.Model, chatOpts.BuilderId != "", chatOpts.Config.HasCapability(uctypes.AICapabilityTools), chatOpts.WidgetAccess)
|
||||
|
||||
if req.TabId != "" {
|
||||
chatOpts.TabStateGenerator = func() (string, []uctypes.ToolDefinition, string, error) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -228,7 +229,7 @@ func SetSecret(name string, value string) error {
|
|||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
secrets[name] = value
|
||||
secrets[name] = strings.TrimRight(value, "\r\n")
|
||||
requestWrite()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ var ExtraTypes = []any{
|
|||
waveobj.MetaTSType{},
|
||||
waveobj.ObjRTInfo{},
|
||||
uctypes.RateLimitInfo{},
|
||||
wconfig.AIModeConfigUpdate{},
|
||||
}
|
||||
|
||||
// add extra type unions to generate here
|
||||
|
|
|
|||
|
|
@ -284,6 +284,10 @@ type AIModeConfigType struct {
|
|||
WaveAIPremium bool `json:"waveai:premium,omitempty"`
|
||||
}
|
||||
|
||||
type AIModeConfigUpdate struct {
|
||||
Configs map[string]AIModeConfigType `json:"configs"`
|
||||
}
|
||||
|
||||
type FullConfigType struct {
|
||||
Settings SettingsType `json:"settings" merge:"meta"`
|
||||
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const (
|
|||
Event_WaveAIRateLimit = "waveai:ratelimit"
|
||||
Event_WaveAppAppGoUpdated = "waveapp:appgoupdated"
|
||||
Event_TsunamiUpdateMeta = "tsunami:updatemeta"
|
||||
Event_AIModeConfig = "waveai:modeconfig"
|
||||
)
|
||||
|
||||
type WaveEvent struct {
|
||||
|
|
|
|||
|
|
@ -428,6 +428,12 @@ func GetWaveAIChatCommand(w *wshutil.WshRpc, data wshrpc.CommandGetWaveAIChatDat
|
|||
return resp, err
|
||||
}
|
||||
|
||||
// command "getwaveaimodeconfig", wshserver.GetWaveAIModeConfigCommand
|
||||
func GetWaveAIModeConfigCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (wconfig.AIModeConfigUpdate, error) {
|
||||
resp, err := sendRpcRequestCallHelper[wconfig.AIModeConfigUpdate](w, "getwaveaimodeconfig", nil, opts)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// command "getwaveairatelimit", wshserver.GetWaveAIRateLimitCommand
|
||||
func GetWaveAIRateLimitCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (*uctypes.RateLimitInfo, error) {
|
||||
resp, err := sendRpcRequestCallHelper[*uctypes.RateLimitInfo](w, "getwaveairatelimit", nil, opts)
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ const (
|
|||
Command_SetConfig = "setconfig"
|
||||
Command_SetConnectionsConfig = "connectionsconfig"
|
||||
Command_GetFullConfig = "getfullconfig"
|
||||
Command_GetWaveAIModeConfig = "getwaveaimodeconfig"
|
||||
Command_RemoteStreamFile = "remotestreamfile"
|
||||
Command_RemoteTarStream = "remotetarstream"
|
||||
Command_RemoteFileInfo = "remotefileinfo"
|
||||
|
|
@ -245,6 +246,7 @@ type WshRpcInterface interface {
|
|||
SetConfigCommand(ctx context.Context, data MetaSettingsType) error
|
||||
SetConnectionsConfigCommand(ctx context.Context, data ConnConfigRequest) error
|
||||
GetFullConfigCommand(ctx context.Context) (wconfig.FullConfigType, error)
|
||||
GetWaveAIModeConfigCommand(ctx context.Context) (wconfig.AIModeConfigUpdate, error)
|
||||
BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error)
|
||||
BlocksListCommand(ctx context.Context, data BlocksListRequest) ([]BlocksListEntry, error)
|
||||
WaveInfoCommand(ctx context.Context) (*WaveInfoData, error)
|
||||
|
|
|
|||
|
|
@ -592,6 +592,12 @@ func (ws *WshServer) GetFullConfigCommand(ctx context.Context) (wconfig.FullConf
|
|||
return watcher.GetFullConfig(), nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) GetWaveAIModeConfigCommand(ctx context.Context) (wconfig.AIModeConfigUpdate, error) {
|
||||
fullConfig := wconfig.GetWatcher().GetFullConfig()
|
||||
resolvedConfigs := aiusechat.ComputeResolvedAIModeConfigs(fullConfig)
|
||||
return wconfig.AIModeConfigUpdate{Configs: resolvedConfigs}, nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) ConnStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus, error) {
|
||||
rtn := conncontroller.GetAllConnStatus()
|
||||
return rtn, nil
|
||||
|
|
|
|||
Loading…
Reference in a new issue