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 {...@@ -122,7 +122,7 @@ import {
122} from "../keybindings";122} from "../keybindings";
123import ChatMarkdown from "./ChatMarkdown";123import ChatMarkdown from "./ChatMarkdown";
124import ThreadTerminalDrawer from "./ThreadTerminalDrawer";124import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
125import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";125import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
126import {126import {
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} />
33243321
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 <ComposerPendingUserInputPanel3390 <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});
39933999
3994interface PendingApprovalsPanelProps {4000interface 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}
40024009
4003const PendingApprovalsPanel = memo(function PendingApprovalsPanel({4010const 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";
40134022
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 <AlertDescription4033 {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 <Button4040 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 once4046 variant="outline"
4038 </Button>4047 disabled={isResponding}
4039 <Button4048 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 session4054 variant="destructive-outline"
4046 </Button>4055 disabled={isResponding}
4047 <Button4056 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 Decline4062 variant="ghost"
4054 </Button>4063 disabled={isResponding}
4055 <Button4064 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});