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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user