11// Copyright 2025, Command Line Inc.
22// SPDX-License-Identifier: Apache-2.0
33
4+ import { Tooltip } from "@/app/element/tooltip" ;
45import { atoms , getSettingsKeyAtom } from "@/app/store/global" ;
56import { RpcApi } from "@/app/store/wshclientapi" ;
67import { TabRpcClient } from "@/app/store/wshrpcutil" ;
78import { cn , fireAndForget , makeIconClass } from "@/util/util" ;
89import { useAtomValue } from "jotai" ;
910import { memo , useRef , useState } from "react" ;
10- import { getFilteredAIModeConfigs } from "./ai-utils" ;
11+ import { getFilteredAIModeConfigs , getModeDisplayName } from "./ai-utils" ;
1112import { WaveAIModel } from "./waveai-model" ;
1213
1314interface 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
5357interface ConfigSection {
5458 sectionName : string ;
55- configs : any [ ] ;
59+ configs : AIModeConfigWithMode [ ] ;
5660 isIncompatible ?: boolean ;
5761}
5862
5963function 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 && (
0 commit comments