@@ -105,6 +105,19 @@ const SESSION_DISCOVERY_INTERVAL_MS = 500;
105105/** Maximum number of session-discovery attempts after send */
106106const SESSION_DISCOVERY_MAX_ATTEMPTS = 6 ;
107107
108+ /** Patterns that identify the "missing API key" error from OpenClaw */
109+ const MISSING_API_KEY_PATTERNS = [
110+ "⚠️ Agent failed before reply: No API key found for provider" ,
111+ "No API key found for provider" ,
112+ "missing_api_key" ,
113+ ] ;
114+
115+ function isMissingApiKeyError ( text : string ) : boolean {
116+ return MISSING_API_KEY_PATTERNS . some ( ( p ) =>
117+ text . toLowerCase ( ) . includes ( p . toLowerCase ( ) ) ,
118+ ) ;
119+ }
120+
108121// ---------------------------------------------------------------------------
109122// Helpers
110123// ---------------------------------------------------------------------------
@@ -630,8 +643,90 @@ function TypingIndicator() {
630643 ) ;
631644}
632645
646+ /** Renders a friendly "No API Key" warning card instead of a raw error */
647+ function ApiKeyWarningCard ( {
648+ t,
649+ } : { t : ReturnType < typeof useTranslation > [ "t" ] } ) {
650+ return (
651+ < div className = "flex items-start gap-3" >
652+ < img
653+ src = { BOT_AVATAR }
654+ alt = ""
655+ className = "h-9 w-9 shrink-0 object-contain -ml-1"
656+ />
657+ < div className = "flex max-w-[44rem] flex-col gap-2" >
658+ < div
659+ style = { {
660+ background : "#FFF7E6" ,
661+ border : "1px solid #FFD591" ,
662+ borderRadius : 12 ,
663+ padding : "14px 16px" ,
664+ } }
665+ >
666+ < div className = "flex items-center gap-2 mb-2" >
667+ < svg
668+ style = { { color : "#FA8C16" , width : 18 , height : 18 , flexShrink : 0 } }
669+ viewBox = "0 0 24 24"
670+ fill = "none"
671+ stroke = "currentColor"
672+ strokeWidth = "2"
673+ strokeLinecap = "round"
674+ strokeLinejoin = "round"
675+ aria-hidden = "true"
676+ >
677+ < path d = "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
678+ < path d = "M12 9v4" />
679+ < path d = "M12 17h.01" />
680+ </ svg >
681+ < span
682+ style = { {
683+ fontWeight : 600 ,
684+ fontSize : 13 ,
685+ color : "#431907" ,
686+ } }
687+ >
688+ { t ( "localChat.missingApiKey.title" ) }
689+ </ span >
690+ </ div >
691+ < p
692+ style = { {
693+ color : "#7A4A0E" ,
694+ fontSize : 13 ,
695+ margin : "0 0 10px 26px" ,
696+ lineHeight : 1.5 ,
697+ } }
698+ >
699+ { t ( "localChat.missingApiKey.description" ) }
700+ </ p >
701+ < a
702+ href = "/workspace/settings?tab=providers"
703+ style = { {
704+ display : "inline-block" ,
705+ marginLeft : 26 ,
706+ background : "#FA8C16" ,
707+ border : "none" ,
708+ borderRadius : 6 ,
709+ color : "#fff" ,
710+ fontSize : 12 ,
711+ fontWeight : 500 ,
712+ padding : "4px 12px" ,
713+ textDecoration : "none" ,
714+ cursor : "pointer" ,
715+ } }
716+ >
717+ { t ( "localChat.missingApiKey.goToSettings" ) }
718+ </ a >
719+ </ div >
720+ </ div >
721+ </ div >
722+ ) ;
723+ }
724+
633725/** Renders a single chat message — supports text, images, and file cards */
634- function ChatBubble ( { msg } : { msg : ChatMsg } ) {
726+ function ChatBubble ( {
727+ msg,
728+ t,
729+ } : { msg : ChatMsg ; t : ReturnType < typeof useTranslation > [ "t" ] } ) {
635730 const isBot = msg . role === "assistant" ;
636731 const time = formatTs ( msg . timestamp ) ;
637732
@@ -645,6 +740,14 @@ function ChatBubble({ msg }: { msg: ChatMsg }) {
645740 ) ;
646741 if ( ! hasContent ) return null ;
647742
743+ // Detect and render the missing-API-key error as a friendly warning card
744+ const firstTextBlock = blocks . find ( ( b ) => b . kind === "text" ) ;
745+ const rawText =
746+ typeof firstTextBlock ?. text === "string" ? firstTextBlock . text : "" ;
747+ if ( isMissingApiKeyError ( rawText ) ) {
748+ return < ApiKeyWarningCard t = { t } /> ;
749+ }
750+
648751 return (
649752 < div
650753 className = { cn (
@@ -1471,7 +1574,7 @@ export function LocalChatPage() {
14711574 < div className = "mx-auto flex w-full max-w-[920px] flex-col gap-5" >
14721575 { /* biome-ignore lint/style/noNonNullAssertion: messages is guaranteed non-null in this branch */ }
14731576 { messages ! . map ( ( msg ) => (
1474- < ChatBubble key = { msg . id } msg = { msg } />
1577+ < ChatBubble key = { msg . id } msg = { msg } t = { t } />
14751578 ) ) }
14761579 { waitingReply && < TypingIndicator /> }
14771580 < div ref = { endRef } />
0 commit comments