Files
codeg/src/components/chat/conversation-shell.tsx
xintaofei f9c0887346 perf(chat): reduce input rerender overhead in conversation shell
Memoize ChatInput to minimize unnecessary updates in the chat input area during frequent parent renders.

Compute localized Claude API retry banner text lazily and only when retry state is present.
2026-04-14 15:12:14 +08:00

228 lines
6.8 KiB
TypeScript

import { useMemo, type ReactNode } from "react"
import { useTranslations } from "next-intl"
import type {
AgentType,
ConnectionStatus,
PromptCapabilitiesInfo,
PromptDraft,
SessionConfigOptionInfo,
SessionModeInfo,
AvailableCommandInfo,
} from "@/lib/types"
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"
interface ConversationShellProps {
status: ConnectionStatus | null
promptCapabilities: PromptCapabilitiesInfo
defaultPath?: string
agentName?: string
error: string | null
claudeApiRetry: ClaudeApiRetryState | null
pendingPermission: PendingPermission | null
pendingQuestion: PendingQuestion | null
onFocus: () => void
onSend: (draft: PromptDraft, modeId?: string | null) => void
onCancel: () => void
onRespondPermission: (requestId: string, optionId: string) => void
onAnswerQuestion: (answer: string) => void
children: ReactNode
modes?: SessionModeInfo[]
configOptions?: SessionConfigOptionInfo[]
modeLoading?: boolean
configOptionsLoading?: boolean
selectorsLoading?: boolean
selectedModeId?: string | null
onModeChange?: (modeId: string) => void
onConfigOptionChange?: (configId: string, valueId: string) => void
agentType?: AgentType | null
availableCommands?: AvailableCommandInfo[] | null
attachmentTabId?: string | null
draftStorageKey?: string | null
hideInput?: boolean
isActive?: boolean
queue?: QueuedMessage[]
onEnqueue?: (draft: PromptDraft, modeId: string | null) => void
onQueueReorder?: (items: QueuedMessage[]) => void
onQueueEdit?: (id: string) => void
onQueueDelete?: (id: string) => void
editingItemId?: string | null
editingDraftText?: string | null
isEditingQueueItem?: boolean
onSaveQueueEdit?: (draft: PromptDraft) => void
onCancelQueueEdit?: () => void
onForkSend?: (draft: PromptDraft, modeId?: string | null) => void
}
export function ConversationShell({
status,
promptCapabilities,
defaultPath,
agentName,
error,
claudeApiRetry,
pendingPermission,
pendingQuestion,
onFocus,
onSend,
onCancel,
onRespondPermission,
onAnswerQuestion,
children,
modes,
configOptions,
modeLoading = false,
configOptionsLoading = false,
selectorsLoading = false,
selectedModeId,
onModeChange,
onConfigOptionChange,
agentType,
availableCommands,
attachmentTabId,
draftStorageKey,
hideInput = false,
isActive,
queue,
onEnqueue,
onQueueReorder,
onQueueEdit,
onQueueDelete,
editingItemId,
editingDraftText,
isEditingQueueItem,
onSaveQueueEdit,
onCancelQueueEdit,
onForkSend,
}: ConversationShellProps) {
const tAcp = useTranslations("Folder.chat.acpConnections")
const retryLineText = useMemo(() => {
const retry = claudeApiRetry
if (!retry) return null
const retryAttempt =
retry.attempt !== null && retry.attempt !== undefined
? Math.trunc(retry.attempt)
: null
const retryMax =
retry.maxRetries !== null && retry.maxRetries !== undefined
? Math.trunc(retry.maxRetries)
: null
const retryDelaySeconds =
retry.retryDelayMs !== null && retry.retryDelayMs !== undefined
? (retry.retryDelayMs / 1000).toFixed(1)
: null
const errorLabel = retry.error ?? tAcp("claudeApiRetry.fallbackError")
const statusLabel =
retry.errorStatus !== null && retry.errorStatus !== undefined
? tAcp("claudeApiRetry.httpStatus", {
status: Math.trunc(retry.errorStatus),
})
: ""
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
return delayLabel !== null
? tAcp("claudeApiRetry.lineWithDelay", {
error: errorLabel,
status: statusLabel,
retry: retryLabel,
delay: delayLabel,
})
: tAcp("claudeApiRetry.line", {
error: errorLabel,
status: statusLabel,
retry: retryLabel,
})
}, [claudeApiRetry, tAcp])
return (
<div className="flex h-full min-h-0 flex-col">
<div className="flex-1 min-h-0">{children}</div>
<PermissionDialog
permission={pendingPermission}
onRespond={onRespondPermission}
/>
<QuestionDialog question={pendingQuestion} onAnswer={onAnswerQuestion} />
{!hideInput && (
<ChatInput
status={status}
promptCapabilities={promptCapabilities}
defaultPath={defaultPath}
agentName={agentName}
onFocus={onFocus}
onSend={onSend}
onCancel={onCancel}
modes={modes}
configOptions={configOptions}
modeLoading={modeLoading}
configOptionsLoading={configOptionsLoading}
selectorsLoading={selectorsLoading}
selectedModeId={selectedModeId}
onModeChange={onModeChange}
onConfigOptionChange={onConfigOptionChange}
agentType={agentType}
availableCommands={availableCommands}
attachmentTabId={attachmentTabId}
draftStorageKey={draftStorageKey}
isActive={isActive}
queue={queue}
onEnqueue={onEnqueue}
onQueueReorder={onQueueReorder}
onQueueEdit={onQueueEdit}
onQueueDelete={onQueueDelete}
editingItemId={editingItemId}
editingDraftText={editingDraftText}
isEditingQueueItem={isEditingQueueItem}
onSaveQueueEdit={onSaveQueueEdit}
onCancelQueueEdit={onCancelQueueEdit}
onForkSend={onForkSend}
/>
)}
{retryLineText && (
<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}
</div>
)}
</div>
)
}