| ... | ... | @@ -122,7 +122,7 @@ import { |
| 122 | 122 | } from "../keybindings"; |
| 123 | 123 | import ChatMarkdown from "./ChatMarkdown"; |
| 124 | 124 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; |
| 125 | | import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; |
| 125 | import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; |
| 126 | 126 | import { |
| 127 | 127 | BotIcon, |
| 128 | 128 | ChevronDownIcon, |
| ... | ... | @@ -134,7 +134,6 @@ import { |
| 134 | 134 | DiffIcon, |
| 135 | 135 | EllipsisIcon, |
| 136 | 136 | FolderClosedIcon, |
| 137 | | InfoIcon, |
| 138 | 137 | LockIcon, |
| 139 | 138 | LockOpenIcon, |
| 140 | 139 | Undo2Icon, |
| ... | ... | @@ -891,8 +890,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 891 | 890 | interactionMode === "plan" && |
| 892 | 891 | latestTurnSettled && |
| 893 | 892 | activeProposedPlan !== null; |
| 893 | const activePendingApproval = pendingApprovals[0] ?? null; |
| 894 | 894 | const hasComposerHeader = |
| 895 | | pendingUserInputs.length > 0 || (showPlanFollowUpPrompt && activeProposedPlan !== null); |
| 895 | activePendingApproval !== null || |
| 896 | pendingUserInputs.length > 0 || |
| 897 | (showPlanFollowUpPrompt && activeProposedPlan !== null); |
| 896 | 898 | useEffect(() => { |
| 897 | 899 | if (!activePendingProgress) { |
| 898 | 900 | return; |
| ... | ... | @@ -3315,11 +3317,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3315 | 3317 | {/* Error banner */} |
| 3316 | 3318 | <ProviderHealthBanner status={activeProviderStatus} /> |
| 3317 | 3319 | <ThreadErrorBanner error={activeThread.error} /> |
| 3318 | | <PendingApprovalsPanel |
| 3319 | | pendingApprovals={pendingApprovals} |
| 3320 | | respondingRequestIds={respondingRequestIds} |
| 3321 | | onRespondToApproval={onRespondToApproval} |
| 3322 | | /> |
| 3323 | 3320 | <PlanModePanel activePlan={activePlan} /> |
| 3324 | 3321 | |
| 3325 | 3322 | {/* Messages */} |
| ... | ... | @@ -3379,7 +3376,16 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3379 | 3376 | onDragLeave={onComposerDragLeave} |
| 3380 | 3377 | onDrop={onComposerDrop} |
| 3381 | 3378 | > |
| 3382 | | {pendingUserInputs.length > 0 ? ( |
| 3379 | {activePendingApproval ? ( |
| 3380 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| 3381 | <ComposerPendingApprovalPanel |
| 3382 | approval={activePendingApproval} |
| 3383 | pendingCount={pendingApprovals.length} |
| 3384 | isResponding={respondingRequestIds.includes(activePendingApproval.requestId)} |
| 3385 | onRespondToApproval={onRespondToApproval} |
| 3386 | /> |
| 3387 | </div> |
| 3388 | ) : pendingUserInputs.length > 0 ? ( |
| 3383 | 3389 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| 3384 | 3390 | <ComposerPendingUserInputPanel |
| 3385 | 3391 | pendingUserInputs={pendingUserInputs} |
| ... | ... | @@ -3991,79 +3997,75 @@ const ProviderHealthBanner = memo(function ProviderHealthBanner({ |
| 3991 | 3997 | ); |
| 3992 | 3998 | }); |
| 3993 | 3999 | |
| 3994 | | interface PendingApprovalsPanelProps { |
| 3995 | | pendingApprovals: PendingApproval[]; |
| 3996 | | respondingRequestIds: ApprovalRequestId[]; |
| 4000 | interface ComposerPendingApprovalPanelProps { |
| 4001 | approval: PendingApproval; |
| 4002 | pendingCount: number; |
| 4003 | isResponding: boolean; |
| 3997 | 4004 | onRespondToApproval: ( |
| 3998 | 4005 | requestId: ApprovalRequestId, |
| 3999 | 4006 | decision: ProviderApprovalDecision, |
| 4000 | 4007 | ) => Promise<void>; |
| 4001 | 4008 | } |
| 4002 | 4009 | |
| 4003 | | const PendingApprovalsPanel = memo(function PendingApprovalsPanel({ |
| 4004 | | pendingApprovals, |
| 4005 | | respondingRequestIds, |
| 4010 | const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({ |
| 4011 | approval, |
| 4012 | pendingCount, |
| 4013 | isResponding, |
| 4006 | 4014 | onRespondToApproval, |
| 4007 | | }: PendingApprovalsPanelProps) { |
| 4008 | | if (pendingApprovals.length === 0) return null; |
| 4009 | | return ( |
| 4010 | | <div className="pt-3 mx-auto max-w-3xl space-y-2"> |
| 4011 | | {pendingApprovals.map((approval) => { |
| 4012 | | const isResponding = respondingRequestIds.includes(approval.requestId); |
| 4015 | }: ComposerPendingApprovalPanelProps) { |
| 4016 | const approvalSummary = |
| 4017 | approval.requestKind === "command" |
| 4018 | ? "Command approval requested" |
| 4019 | : approval.requestKind === "file-read" |
| 4020 | ? "File-read approval requested" |
| 4021 | : "File-change approval requested"; |
| 4013 | 4022 | |
| 4014 | | return ( |
| 4015 | | <Alert variant="warning" key={approval.requestId}> |
| 4016 | | <InfoIcon /> |
| 4017 | | <AlertTitle className="text-xs"> |
| 4018 | | {approval.requestKind === "command" |
| 4019 | | ? "Command approval requested" |
| 4020 | | : approval.requestKind === "file-read" |
| 4021 | | ? "File-read approval requested" |
| 4022 | | : "File-change approval requested"} |
| 4023 | | </AlertTitle> |
| 4024 | | <AlertDescription |
| 4025 | | className="truncate block font-mono text-[11px]" |
| 4026 | | title={approval.detail} |
| 4027 | | > |
| 4028 | | {approval.detail} |
| 4029 | | </AlertDescription> |
| 4030 | | <AlertAction className="col-start-2! -col-end-1! mt-1.5 sm:row-start-auto sm:row-end-auto"> |
| 4031 | | <Button |
| 4032 | | size="xs" |
| 4033 | | variant="default" |
| 4034 | | disabled={isResponding} |
| 4035 | | onClick={() => void onRespondToApproval(approval.requestId, "accept")} |
| 4036 | | > |
| 4037 | | Approve once |
| 4038 | | </Button> |
| 4039 | | <Button |
| 4040 | | size="xs" |
| 4041 | | variant="outline" |
| 4042 | | disabled={isResponding} |
| 4043 | | onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")} |
| 4044 | | > |
| 4045 | | Always allow this session |
| 4046 | | </Button> |
| 4047 | | <Button |
| 4048 | | size="xs" |
| 4049 | | variant="destructive-outline" |
| 4050 | | disabled={isResponding} |
| 4051 | | onClick={() => void onRespondToApproval(approval.requestId, "decline")} |
| 4052 | | > |
| 4053 | | Decline |
| 4054 | | </Button> |
| 4055 | | <Button |
| 4056 | | size="xs" |
| 4057 | | variant="ghost" |
| 4058 | | disabled={isResponding} |
| 4059 | | onClick={() => void onRespondToApproval(approval.requestId, "cancel")} |
| 4060 | | > |
| 4061 | | Cancel turn |
| 4062 | | </Button> |
| 4063 | | </AlertAction> |
| 4064 | | </Alert> |
| 4065 | | ); |
| 4066 | | })} |
| 4023 | return ( |
| 4024 | <div className="px-4 py-3.5 sm:px-5 sm:py-4"> |
| 4025 | <div className="flex flex-wrap items-center gap-2"> |
| 4026 | <span className="uppercase text-sm tracking-[0.2em]">PENDING APPROVAL</span> |
| 4027 | <span className="text-sm font-medium">{approvalSummary}</span> |
| 4028 | {pendingCount > 1 ? ( |
| 4029 | <span className="text-xs text-muted-foreground">1/{pendingCount}</span> |
| 4030 | ) : null} |
| 4031 | </div> |
| 4032 | <p className="mt-2 block truncate font-mono text-[11px] text-muted-foreground" title={approval.detail}> |
| 4033 | {approval.detail} |
| 4034 | </p> |
| 4035 | <div className="mt-3 flex flex-wrap items-center justify-end gap-2"> |
| 4036 | <Button |
| 4037 | size="xs" |
| 4038 | variant="default" |
| 4039 | disabled={isResponding} |
| 4040 | onClick={() => void onRespondToApproval(approval.requestId, "accept")} |
| 4041 | > |
| 4042 | Approve once |
| 4043 | </Button> |
| 4044 | <Button |
| 4045 | size="xs" |
| 4046 | variant="outline" |
| 4047 | disabled={isResponding} |
| 4048 | onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")} |
| 4049 | > |
| 4050 | Always allow this session |
| 4051 | </Button> |
| 4052 | <Button |
| 4053 | size="xs" |
| 4054 | variant="destructive-outline" |
| 4055 | disabled={isResponding} |
| 4056 | onClick={() => void onRespondToApproval(approval.requestId, "decline")} |
| 4057 | > |
| 4058 | Decline |
| 4059 | </Button> |
| 4060 | <Button |
| 4061 | size="xs" |
| 4062 | variant="ghost" |
| 4063 | disabled={isResponding} |
| 4064 | onClick={() => void onRespondToApproval(approval.requestId, "cancel")} |
| 4065 | > |
| 4066 | Cancel turn |
| 4067 | </Button> |
| 4068 | </div> |
| 4067 | 4069 | </div> |
| 4068 | 4070 | ); |
| 4069 | 4071 | }); |