minor v0.13 fixes (#2649)

This commit is contained in:
Mike Sawka 2025-12-08 21:58:54 -08:00 committed by GitHub
parent 4ac5e0b332
commit c9c6192fc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 261 additions and 49 deletions

View file

@ -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()

View file

@ -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"})`;
}

View file

@ -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 && (

View file

@ -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" });

View file

@ -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) => {

View file

@ -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

View file

@ -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;
}

View file

@ -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 };

View file

@ -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) => {

View file

@ -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);

View file

@ -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 {};

View file

@ -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

View file

@ -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
View file

@ -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": [

View file

@ -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,
})
}

View file

@ -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:

View file

@ -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) {

View file

@ -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
}

View file

@ -53,6 +53,7 @@ var ExtraTypes = []any{
waveobj.MetaTSType{},
waveobj.ObjRTInfo{},
uctypes.RateLimitInfo{},
wconfig.AIModeConfigUpdate{},
}
// add extra type unions to generate here

View file

@ -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"`

View file

@ -21,6 +21,7 @@ const (
Event_WaveAIRateLimit = "waveai:ratelimit"
Event_WaveAppAppGoUpdated = "waveapp:appgoupdated"
Event_TsunamiUpdateMeta = "tsunami:updatemeta"
Event_AIModeConfig = "waveai:modeconfig"
)
type WaveEvent struct {

View file

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

View file

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

View file

@ -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