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