aiprompts/aimodesconfig.md
Wave Terminal's AI modes configuration system allows users to define custom AI assistants with different providers, models, and capabilities. The configuration is stored in ~/.waveterm/config/waveai.json and provides a flexible way to configure multiple AI modes that appear in the Wave AI panel.
Key Design Decisions:
waveai@quick, waveai@balanced, waveai@deep) are read-only in visual editorOPENAI_KEY, OPENROUTER_KEY)Location: pkg/wconfig/settingsconfig.go:264-284
type AIModeConfigType struct {
// Display Configuration
DisplayName string `json:"display:name"` // Required
DisplayOrder float64 `json:"display:order,omitempty"`
DisplayIcon string `json:"display:icon,omitempty"`
DisplayShortDesc string `json:"display:shortdesc,omitempty"`
DisplayDescription string `json:"display:description,omitempty"`
// Provider & Model
Provider string `json:"ai:provider,omitempty"` // wave, google, openrouter, openai, azure, azure-legacy, custom
APIType string `json:"ai:apitype"` // Required: anthropic-messages, openai-responses, openai-chat
Model string `json:"ai:model"` // Required
// AI Behavior
ThinkingLevel string `json:"ai:thinkinglevel,omitempty"` // low, medium, high
Capabilities []string `json:"ai:capabilities,omitempty"` // pdfs, images, tools
// Connection Details
Endpoint string `json:"ai:endpoint,omitempty"`
APIVersion string `json:"ai:apiversion,omitempty"`
APIToken string `json:"ai:apitoken,omitempty"`
APITokenSecretName string `json:"ai:apitokensecretname,omitempty"`
// Azure-Specific
AzureResourceName string `json:"ai:azureresourcename,omitempty"`
AzureDeployment string `json:"ai:azuredeployment,omitempty"`
// Wave AI Specific
WaveAICloud bool `json:"waveai:cloud,omitempty"`
WaveAIPremium bool `json:"waveai:premium,omitempty"`
}
Storage: FullConfigType.WaveAIModes - map[string]AIModeConfigType
Keys follow pattern: provider@modename (e.g., waveai@quick, openai@gpt4)
Defined in: pkg/aiusechat/uctypes/uctypes.go:27-35
wave - Wave AI Cloud service
waveai:cloud = true, endpoint from env or defaulthttps://cfapi.waveterm.dev/api/waveaiopenai - OpenAI API
https://api.openai.com/v1openai-chatopenai-responsesopenrouter - OpenRouter service
https://openrouter.ai/api/v1, API type openai-chatgoogle - Google AI (Gemini, etc.)
azure - Azure OpenAI (new unified API)
v1, endpoint from resource namehttps://{resource}.openai.azure.com/openai/v1/{responses|chat/completions}azure-legacy - Azure OpenAI (legacy chat completions)
2025-04-01-preview, API type openai-chathttps://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={version}AzureResourceName and AzureDeploymentcustom - Custom provider
Location: pkg/wconfig/defaultconfig/waveai.json
Ships with three Wave AI modes:
waveai@quick - Fast responses (gpt-5-mini, low thinking)waveai@balanced - Balanced (gpt-5.1, low thinking) [premium]waveai@deep - Maximum capability (gpt-5.1, medium thinking) [premium]Location: frontend/app/view/waveconfig/waveaivisual.tsx
Currently shows placeholder: "Visual editor coming soon..."
The component receives:
model: WaveConfigViewModel - Access to config file operationsSecretsContent for list/detail views┌─────────────────────────────────────────────────────────┐
│ Wave AI Modes Configuration │
│ ┌───────────────┐ ┌──────────────────────────────┐ │
│ │ │ │ │ │
│ │ Mode List │ │ Mode Editor/Viewer │ │
│ │ │ │ │ │
│ │ [Quick] │ │ Provider: [wave ▼] │ │
│ │ [Balanced] │ │ │ │
│ │ [Deep] │ │ Display Configuration │ │
│ │ [Custom] │ │ ├─ Name: ... │ │
│ │ │ │ ├─ Icon: ... │ │
│ │ [+ Add New] │ │ └─ Description: ... │ │
│ │ │ │ │ │
│ │ │ │ Provider Configuration │ │
│ │ │ │ (Provider-specific fields) │ │
│ │ │ │ │ │
│ │ │ │ [Save] [Delete] [Cancel] │ │
│ └───────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
WaveAIVisualContent
├─ ModeList (left panel)
│ ├─ Header with "Add New Mode" button
│ ├─ List of existing modes (sorted by display:order)
│ │ └─ ModeListItem (icon, name, short desc, provider badge)
│ └─ Empty state if no modes
│
└─ ModeEditor (right panel)
├─ Provider selector dropdown (when creating/editing)
├─ Display section (common to all providers)
│ ├─ Name input (required)
│ ├─ Icon picker (optional)
│ ├─ Display order (optional, number)
│ ├─ Short description (optional)
│ └─ Description textarea (optional)
│
├─ Provider Configuration section (dynamic based on provider)
│ └─ [Provider-specific form fields]
│
└─ Action buttons (Save, Delete, Cancel)
wave)Read-only/Auto-managed:
User-configurable:
openai)Auto-managed:
OPENAI_KEYUser-configurable:
openrouter)Auto-managed:
OPENROUTER_KEYUser-configurable:
azure)Auto-managed:
AZURE_KEYUser-configurable:
azure-legacy)Auto-managed:
AZURE_KEYUser-configurable:
google)Auto-managed:
GOOGLE_KEYUser-configurable:
custom)User must specify everything:
Load JSON → Parse → Render Visual Editor
↓
User Edits Mode → Update fileContentAtom (JSON string)
↓
Click Save → Existing save logic validates & writes
Simplified Operations:
fileContentAtom JSON string into mode objects for displayfileContentAtom → marks as editedfileContentAtomfileContentAtommodel.saveFile() handles validation and writeMode Key Generation:
function generateModeKey(provider: string, model: string): string {
// Try semantic key first: provider@model-sanitized
const sanitized = model.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
const semanticKey = `${provider}@${sanitized}`;
// Check for collision, if exists append random suffix
if (existingModes[semanticKey]) {
const randomId = crypto.randomUUID().slice(-6);
return `${provider}@${sanitized}-${randomId}`;
}
return semanticKey;
}
// Examples: openai@gpt-4o, openrouter@claude-3-5-sonnet, azure@custom-fb4a2c
Secret Naming Convention:
// Fixed secret names per provider (except custom)
const SECRET_NAMES = {
openai: "OPENAI_KEY",
openrouter: "OPENROUTER_KEY",
azure: "AZURE_KEY",
"azure-legacy": "AZURE_KEY",
google: "GOOGLE_KEY",
// custom provider: user specifies their own secret name
} as const;
function getSecretName(provider: string, customSecretName?: string): string {
if (provider === "custom") {
return customSecretName || "CUSTOM_API_KEY";
}
return SECRET_NAMES[provider];
}
Secret Status Indicator: Display next to API Key field for providers that need one:
Secret Modal:
┌─────────────────────────────────────┐
│ Set API Key for OpenAI │
│ │
│ Secret Name: OPENAI_KEY │
│ [read-only for non-custom] │
│ │
│ API Key: │
│ [********************] [Show/Hide]│
│ │
│ [Cancel] [Save] │
└─────────────────────────────────────┘
Modal Behavior:
Integration with Mode Editor:
GetSecretsCommand resultdisplay:order (ascending)Raw JSON Editor Option:
display:name, ai:apitype, ai:model)ai:apitokensecretnameai:apitoken^[[email protected]]+$display:name, ai:apitype, ai:model^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ (1-63 chars)When provider changes or model changes:
// Main container
WaveAIVisualContent
// Left panel
ModeList
├─ ModeListItem (icon, name, provider badge, premium badge, drag handle)
└─ AddModeButton
// Right panel - viewer
ModeViewer
├─ ModeHeader (name, icon, actions)
├─ DisplaySection (read-only view of display fields)
├─ ProviderSection (read-only view of provider config)
└─ EditButton
// Right panel - editor
ModeEditor
├─ ProviderSelector (dropdown, only for new modes)
├─ DisplayFieldsForm
├─ ProviderFieldsForm (dynamic based on provider)
│ ├─ WaveProviderForm
│ ├─ OpenAIProviderForm
│ ├─ OpenRouterProviderForm
│ ├─ AzureProviderForm
│ ├─ AzureLegacyProviderForm
│ ├─ GoogleProviderForm
│ └─ CustomProviderForm
└─ ActionButtons (Edit Raw JSON, Delete, Cancel)
// Modals
RawJSONModal
├─ Title ("Edit Raw JSON: {mode name}")
├─ MonacoEditor (JSON, single mode object)
├─ ValidationErrors (inline display)
└─ Actions (Cancel, Save)
// Shared components
SecretSelector (dropdown + link to secrets)
InfoTooltip (explains auto-configured fields)
ProviderBadge (visual indicator)
IconPicker (select from available icons)
DragHandle (for reordering modes in list)
Drag & Drop for Reordering:
// Reordering updates display:order automatically
function handleModeReorder(draggedKey: string, targetKey: string) {
const modes = parseAIModes(fileContent);
const modesList = Object.entries(modes)
.sort((a, b) => (a[1]["display:order"] || 0) - (b[1]["display:order"] || 0));
// Find indices
const draggedIndex = modesList.findIndex(([k]) => k === draggedKey);
const targetIndex = modesList.findIndex(([k]) => k === targetKey);
// Recalculate display:order for all modes
const newOrder = [...modesList];
newOrder.splice(draggedIndex, 1);
newOrder.splice(targetIndex, 0, modesList[draggedIndex]);
// Assign new order values (0, 10, 20, 30...)
newOrder.forEach(([key, mode], index) => {
modes[key] = { ...mode, "display:order": index * 10 };
});
updateFileContent(JSON.stringify(modes, null, 2));
}
No new atoms needed! Visual editor uses existing fileContentAtom:
// Use existing atoms from WaveConfigViewModel:
// - fileContentAtom (contains JSON string)
// - hasEditedAtom (tracks if modified)
// - errorMessageAtom (for errors)
// Visual editor parses fileContentAtom on render:
function parseAIModes(jsonString: string): Record<string, AIModeConfigType> | null {
try {
return JSON.parse(jsonString);
} catch {
return null; // Show "invalid JSON" error
}
}
// Updates modify fileContentAtom:
function updateMode(key: string, mode: AIModeConfigType) {
const modes = parseAIModes(globalStore.get(model.fileContentAtom));
if (!modes) return;
modes[key] = mode;
const newJson = JSON.stringify(modes, null, 2);
globalStore.set(model.fileContentAtom, newJson);
globalStore.set(model.hasEditedAtom, true);
}
// Secrets use existing model methods:
// - model.refreshSecrets() - already exists
// - RpcApi.GetSecretsCommand() - check if secret exists
// - RpcApi.SetSecretsCommand() - set secret value
Component State (useState):
// In WaveAIVisualContent component:
const [selectedModeKey, setSelectedModeKey] = useState<string | null>(null);
const [isAddingMode, setIsAddingMode] = useState(false);
const [showSecretModal, setShowSecretModal] = useState(false);
const [secretModalProvider, setSecretModalProvider] = useState<string>("");
fileContentAtom JSON into modes on renderdisplay:orderfileContentAtom JSONdisplay:name, ai:apitype, ai:model)display:order for all affected modesdisplay:order valuesfileContentAtom on every read/writewaveai@ → read-onlyAIModeConfigType from gotypes.d.tsfileContentAtom, hasEditedAtom)fileContentAtom changes externallyThis design provides a user-friendly way to configure AI modes without directly editing JSON, while still maintaining the power and flexibility of the underlying system.