authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-06 08:59:10-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-06 09:03:16-08:00
loge28ad181066edece29f98aa717c01fc9f347e33a
tree13cc2ca092fcaf66b04adb2c94fd35ff08accf15
parent525411101dc6c2f68e4950875bebb001c80b90f8

Inline pending approvals in composer header

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 76 insertions(+), 74 deletions(-)

apps/web/src/components/ChatView.tsx+76-74
......@@ -122,7 +122,7 @@ import {
122122} from "../keybindings";
123123import ChatMarkdown from "./ChatMarkdown";
124124import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
125import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
125import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
126126import {
127127 BotIcon,
128128 ChevronDownIcon,
......@@ -134,7 +134,6 @@ import {
134134 DiffIcon,
135135 EllipsisIcon,
136136 FolderClosedIcon,
137 InfoIcon,
138137 LockIcon,
139138 LockOpenIcon,
140139 Undo2Icon,
......@@ -891,8 +890,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
891890 interactionMode === "plan" &&
892891 latestTurnSettled &&
893892 activeProposedPlan !== null;
893 const activePendingApproval = pendingApprovals[0] ?? null;
894894 const hasComposerHeader =
895 pendingUserInputs.length > 0 || (showPlanFollowUpPrompt && activeProposedPlan !== null);
895 activePendingApproval !== null ||
896 pendingUserInputs.length > 0 ||
897 (showPlanFollowUpPrompt && activeProposedPlan !== null);
896898 useEffect(() => {
897899 if (!activePendingProgress) {
898900 return;
......@@ -3315,11 +3317,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
33153317 {/* Error banner */}
33163318 <ProviderHealthBanner status={activeProviderStatus} />
33173319 <ThreadErrorBanner error={activeThread.error} />
3318 <PendingApprovalsPanel
3319 pendingApprovals={pendingApprovals}
3320 respondingRequestIds={respondingRequestIds}
3321 onRespondToApproval={onRespondToApproval}
3322 />
33233320 <PlanModePanel activePlan={activePlan} />
33243321
33253322 {/* Messages */}
......@@ -3379,7 +3376,16 @@ export default function ChatView({ threadId }: ChatViewProps) {
33793376 onDragLeave={onComposerDragLeave}
33803377 onDrop={onComposerDrop}
33813378 >
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 ? (
33833389 <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
33843390 <ComposerPendingUserInputPanel
33853391 pendingUserInputs={pendingUserInputs}
......@@ -3991,79 +3997,75 @@ const ProviderHealthBanner = memo(function ProviderHealthBanner({
39913997 );
39923998});
39933999
3994interface PendingApprovalsPanelProps {
3995 pendingApprovals: PendingApproval[];
3996 respondingRequestIds: ApprovalRequestId[];
4000interface ComposerPendingApprovalPanelProps {
4001 approval: PendingApproval;
4002 pendingCount: number;
4003 isResponding: boolean;
39974004 onRespondToApproval: (
39984005 requestId: ApprovalRequestId,
39994006 decision: ProviderApprovalDecision,
40004007 ) => Promise<void>;
40014008}
40024009
4003const PendingApprovalsPanel = memo(function PendingApprovalsPanel({
4004 pendingApprovals,
4005 respondingRequestIds,
4010const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({
4011 approval,
4012 pendingCount,
4013 isResponding,
40064014 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";
40134022
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>
40674069 </div>
40684070 );
40694071});