feat(desktop): add Configure button in chat error banner

When the agent fails due to missing API key, the error banner now
shows a "Configure" button that opens the same ApiKeyDialog (or
OAuthDialog) used on the home page. After successful configuration
the error clears and the user can immediately start chatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jiang Bohan 2026-02-09 13:51:55 +08:00
parent 8d32a06b5c
commit ed681a96bf
3 changed files with 109 additions and 23 deletions

View file

@ -1,6 +1,10 @@
import { useState, useCallback } from 'react'
import { Loading } from '@multica/ui/components/ui/loading'
import { ChatView } from '@multica/ui/components/chat-view'
import { useLocalChat } from '../hooks/use-local-chat'
import { useProvider } from '../hooks/use-provider'
import { ApiKeyDialog } from './api-key-dialog'
import { OAuthDialog } from './oauth-dialog'
export function LocalChat() {
const {
@ -17,8 +21,41 @@ export function LocalChat() {
sendMessage,
loadMore,
resolveApproval,
clearError,
} = useLocalChat()
const { providers, current, setProvider: switchProvider, refresh: refreshProviders } = useProvider()
// Provider config dialog state
const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false)
const [oauthDialogOpen, setOauthDialogOpen] = useState(false)
const handleConfigureProvider = useCallback(() => {
const providerId = current?.provider
if (!providerId) return
const meta = providers.find((p) => p.id === providerId)
if (!meta) return
if (meta.authMethod === 'oauth') {
setOauthDialogOpen(true)
} else {
setApiKeyDialogOpen(true)
}
}, [current, providers])
const handleProviderConfigSuccess = useCallback(async () => {
const providerId = current?.provider
if (!providerId) return
await refreshProviders()
await switchProvider(providerId)
clearError()
}, [current, refreshProviders, switchProvider, clearError])
// Derive provider info for dialogs
const currentMeta = current ? providers.find((p) => p.id === current.provider) : null
if (initError) {
return (
<div className="flex-1 flex items-center justify-center text-sm text-destructive">
@ -36,19 +73,48 @@ export function LocalChat() {
)
}
// Show "Configure" button when error is about provider/API key
const errorAction = error?.code === 'AGENT_ERROR' && currentMeta
? { label: 'Configure', onClick: handleConfigureProvider }
: undefined
return (
<ChatView
messages={messages}
streamingIds={streamingIds}
isLoading={isLoading}
isLoadingHistory={isLoadingHistory}
isLoadingMore={isLoadingMore}
hasMore={hasMore}
error={error}
pendingApprovals={pendingApprovals}
sendMessage={sendMessage}
loadMore={loadMore}
resolveApproval={resolveApproval}
/>
<>
<ChatView
messages={messages}
streamingIds={streamingIds}
isLoading={isLoading}
isLoadingHistory={isLoadingHistory}
isLoadingMore={isLoadingMore}
hasMore={hasMore}
error={error}
pendingApprovals={pendingApprovals}
sendMessage={sendMessage}
loadMore={loadMore}
resolveApproval={resolveApproval}
errorAction={errorAction}
/>
{currentMeta && currentMeta.authMethod === 'api-key' && (
<ApiKeyDialog
open={apiKeyDialogOpen}
onOpenChange={setApiKeyDialogOpen}
providerId={currentMeta.id}
providerName={currentMeta.name}
onSuccess={handleProviderConfigSuccess}
/>
)}
{currentMeta && currentMeta.authMethod === 'oauth' && (
<OAuthDialog
open={oauthDialogOpen}
onOpenChange={setOauthDialogOpen}
providerId={currentMeta.id}
providerName={currentMeta.name}
loginCommand={currentMeta.loginCommand}
onSuccess={handleProviderConfigSuccess}
/>
)}
</>
)
}

View file

@ -142,6 +142,10 @@ export function useLocalChat() {
[],
)
const clearError = useCallback(() => {
chatRef.current.setError(null)
}, [])
return {
agentId,
initError,
@ -156,5 +160,6 @@ export function useLocalChat() {
sendMessage,
loadMore,
resolveApproval,
clearError,
}
}

View file

@ -38,6 +38,8 @@ export interface ChatViewProps {
loadMore?: () => void;
resolveApproval: (approvalId: string, decision: "allow-once" | "allow-always" | "deny") => void;
onDisconnect?: () => void;
/** Optional action button in the error banner (e.g. "Configure Provider") */
errorAction?: { label: string; onClick: () => void };
}
export function ChatView({
@ -53,6 +55,7 @@ export function ChatView({
loadMore,
resolveApproval,
onDisconnect,
errorAction,
}: ChatViewProps) {
const mainRef = useRef<HTMLElement>(null);
const sentinelRef = useRef<HTMLDivElement>(null);
@ -219,16 +222,28 @@ export function ChatView({
<div className="container px-4" role="alert" aria-live="polite">
<div className="rounded-lg bg-destructive/5 border border-destructive/15 text-xs px-3 py-2 flex items-center justify-between gap-3">
<span className="text-foreground leading-snug">{error.message}</span>
{onDisconnect && (
<Button
variant="destructive"
size="sm"
onClick={onDisconnect}
className="shrink-0 text-xs h-7 px-2.5"
>
Disconnect
</Button>
)}
<div className="flex items-center gap-2 shrink-0">
{errorAction && (
<Button
variant="outline"
size="sm"
onClick={errorAction.onClick}
className="shrink-0 text-xs h-7 px-2.5"
>
{errorAction.label}
</Button>
)}
{onDisconnect && (
<Button
variant="destructive"
size="sm"
onClick={onDisconnect}
className="shrink-0 text-xs h-7 px-2.5"
>
Disconnect
</Button>
)}
</div>
</div>
</div>
)}