feat(acp): surface Claude API retry state in chat input

Enable raw Claude SDK forwarding for ACP sessions and emit only system/api_retry events to the frontend.

Show a localized single-line retry banner with loading under the conversation input, including error details and retry progress.
This commit is contained in:
xintaofei
2026-04-14 14:59:32 +08:00
parent 77e46d80f8
commit f9923df1fe
17 changed files with 492 additions and 17 deletions

View File

@@ -1,4 +1,5 @@
import type { ReactNode } from "react"
import { useTranslations } from "next-intl"
import type {
AgentType,
ConnectionStatus,
@@ -11,8 +12,10 @@ import type {
import type {
PendingPermission,
PendingQuestion,
ClaudeApiRetryState,
} from "@/contexts/acp-connections-context"
import type { QueuedMessage } from "@/hooks/use-message-queue"
import { Loader2 } from "lucide-react"
import { ChatInput } from "@/components/chat/chat-input"
import { PermissionDialog } from "@/components/chat/permission-dialog"
import { QuestionDialog } from "@/components/chat/question-dialog"
@@ -23,6 +26,7 @@ interface ConversationShellProps {
defaultPath?: string
agentName?: string
error: string | null
claudeApiRetry: ClaudeApiRetryState | null
pendingPermission: PendingPermission | null
pendingQuestion: PendingQuestion | null
onFocus: () => void
@@ -64,6 +68,7 @@ export function ConversationShell({
defaultPath,
agentName,
error,
claudeApiRetry,
pendingPermission,
pendingQuestion,
onFocus,
@@ -98,6 +103,63 @@ export function ConversationShell({
onCancelQueueEdit,
onForkSend,
}: ConversationShellProps) {
const tAcp = useTranslations("Folder.chat.acpConnections")
const retry = claudeApiRetry
const retryAttemptRaw = retry?.attempt
const retryMaxRaw = retry?.maxRetries
const retryDelayMsRaw = retry?.retryDelayMs
const retryErrorStatusRaw = retry?.errorStatus
const retryAttempt =
retryAttemptRaw !== null && retryAttemptRaw !== undefined
? Math.trunc(retryAttemptRaw)
: null
const retryMax =
retryMaxRaw !== null && retryMaxRaw !== undefined
? Math.trunc(retryMaxRaw)
: null
const retryDelaySeconds =
retryDelayMsRaw !== null && retryDelayMsRaw !== undefined
? (retryDelayMsRaw / 1000).toFixed(1)
: null
const errorLabel = retry?.error ?? tAcp("claudeApiRetry.fallbackError")
const statusLabel =
retryErrorStatusRaw !== null && retryErrorStatusRaw !== undefined
? tAcp("claudeApiRetry.httpStatus", {
status: Math.trunc(retryErrorStatusRaw),
})
: ""
const retryLabel =
retryAttempt !== null && retryMax !== null
? tAcp("claudeApiRetry.retryingWithMax", {
attempt: retryAttempt,
max: retryMax,
})
: retryAttempt !== null
? tAcp("claudeApiRetry.retryingAttempt", {
attempt: retryAttempt,
})
: tAcp("claudeApiRetry.retrying")
const delayLabel =
retryDelaySeconds !== null
? tAcp("claudeApiRetry.nextRetryIn", {
seconds: retryDelaySeconds,
})
: null
const retryLineText =
delayLabel !== null
? tAcp("claudeApiRetry.lineWithDelay", {
error: errorLabel,
status: statusLabel,
retry: retryLabel,
delay: delayLabel,
})
: tAcp("claudeApiRetry.line", {
error: errorLabel,
status: statusLabel,
retry: retryLabel,
})
return (
<div className="flex h-full min-h-0 flex-col">
<div className="flex-1 min-h-0">{children}</div>
@@ -145,6 +207,17 @@ export function ConversationShell({
/>
)}
{claudeApiRetry && (
<div className="border-t border-destructive/20 bg-destructive/5 px-4 py-2 text-xs text-destructive">
<div className="flex items-center gap-2 font-medium">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
{retryLineText}
</span>
</div>
</div>
)}
{error && (
<div className="px-4 py-2 text-xs text-destructive bg-destructive/5 border-t border-destructive/20">
{error}

View File

@@ -877,6 +877,7 @@ const ConversationTabView = memo(function ConversationTabView({
defaultPath={workingDirForConnection}
agentName={AGENT_LABELS[selectedAgent]}
error={conn.error}
claudeApiRetry={conn.claudeApiRetry}
pendingPermission={conn.pendingPermission}
pendingQuestion={conn.pendingQuestion}
onFocus={handleFocus}