From 74f67e5e1de63bb18cdaeb25df27e1442a8a2a78 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Mar 2026 08:59:10 -0800 Subject: [PATCH 1/2] Inline pending approvals in composer header Co-authored-by: codex --- apps/web/src/components/ChatView.tsx | 150 ++++++++++++++------------- 1 file changed, 76 insertions(+), 74 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f80a0553e3f0..1380a8b9d4b3 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -122,7 +122,7 @@ import { } from "../keybindings"; import ChatMarkdown from "./ChatMarkdown"; import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; -import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { BotIcon, ChevronDownIcon, @@ -134,7 +134,6 @@ import { DiffIcon, EllipsisIcon, FolderClosedIcon, - InfoIcon, LockIcon, LockOpenIcon, Undo2Icon, @@ -891,8 +890,11 @@ export default function ChatView({ threadId }: ChatViewProps) { interactionMode === "plan" && latestTurnSettled && activeProposedPlan !== null; + const activePendingApproval = pendingApprovals[0] ?? null; const hasComposerHeader = - pendingUserInputs.length > 0 || (showPlanFollowUpPrompt && activeProposedPlan !== null); + activePendingApproval !== null || + pendingUserInputs.length > 0 || + (showPlanFollowUpPrompt && activeProposedPlan !== null); useEffect(() => { if (!activePendingProgress) { return; @@ -3315,11 +3317,6 @@ export default function ChatView({ threadId }: ChatViewProps) { {/* Error banner */} - {/* Messages */} @@ -3379,7 +3376,16 @@ export default function ChatView({ threadId }: ChatViewProps) { onDragLeave={onComposerDragLeave} onDrop={onComposerDrop} > - {pendingUserInputs.length > 0 ? ( + {activePendingApproval ? ( +
+ +
+ ) : pendingUserInputs.length > 0 ? (
Promise; } -const PendingApprovalsPanel = memo(function PendingApprovalsPanel({ - pendingApprovals, - respondingRequestIds, +const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({ + approval, + pendingCount, + isResponding, onRespondToApproval, -}: PendingApprovalsPanelProps) { - if (pendingApprovals.length === 0) return null; - return ( -
- {pendingApprovals.map((approval) => { - const isResponding = respondingRequestIds.includes(approval.requestId); +}: ComposerPendingApprovalPanelProps) { + const approvalSummary = + approval.requestKind === "command" + ? "Command approval requested" + : approval.requestKind === "file-read" + ? "File-read approval requested" + : "File-change approval requested"; - return ( - - - - {approval.requestKind === "command" - ? "Command approval requested" - : approval.requestKind === "file-read" - ? "File-read approval requested" - : "File-change approval requested"} - - - {approval.detail} - - - - - - - - - ); - })} + return ( +
+
+ PENDING APPROVAL + {approvalSummary} + {pendingCount > 1 ? ( + 1/{pendingCount} + ) : null} +
+

+ {approval.detail} +

+
+ + + + +
); }); From 005dd9a12dd4c7059aa5afd7a44c8973e27ec735 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 6 Mar 2026 09:01:10 -0800 Subject: [PATCH 2/2] Streamline composer behavior during pending approvals - Treat pending approvals as a dedicated composer state - Disable prompt input and hide command menu/image chips while approval is active - Move approval decision buttons into footer actions and simplify the header panel --- apps/web/src/components/ChatView.tsx | 561 ++++++++++++++------------- 1 file changed, 293 insertions(+), 268 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 1380a8b9d4b3..446831c8a9c6 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -891,8 +891,9 @@ export default function ChatView({ threadId }: ChatViewProps) { latestTurnSettled && activeProposedPlan !== null; const activePendingApproval = pendingApprovals[0] ?? null; + const isComposerApprovalState = activePendingApproval !== null; const hasComposerHeader = - activePendingApproval !== null || + isComposerApprovalState || pendingUserInputs.length > 0 || (showPlanFollowUpPrompt && activeProposedPlan !== null); useEffect(() => { @@ -3381,8 +3382,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
) : pendingUserInputs.length > 0 ? ( @@ -3411,7 +3410,7 @@ export default function ChatView({ threadId }: ChatViewProps) { hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4", )} > - {composerMenuOpen && ( + {composerMenuOpen && !isComposerApprovalState && (
)} - {pendingUserInputs.length === 0 && composerImages.length > 0 && ( + {!isComposerApprovalState && pendingUserInputs.length === 0 && composerImages.length > 0 && (
{composerImages.map((image) => (
{/* Bottom toolbar */} -
-
- {/* Provider/model picker */} - + - - {selectedProvider === "codex" && selectedEffort != null ? ( - <> - - - - ) : null} - - {/* Divider */} - - - {/* Interaction mode toggle */} - - - {/* Divider */} - - - {/* Runtime mode toggle */} -
+ ) : ( +
+
+ {/* Provider/model picker */} + - {/* Right side: send / stop button */} -
- {isPreparingWorktree ? ( - Preparing worktree... - ) : null} - {activePendingProgress ? ( -
- {activePendingProgress.questionIndex > 0 ? ( - - ) : null} - -
- ) : phase === "running" ? ( - - ) : pendingUserInputs.length === 0 ? ( - showPlanFollowUpPrompt ? ( - prompt.trim().length > 0 ? ( + + + {interactionMode === "plan" ? "Plan" : "Chat"} + + + + {/* Divider */} + + + {/* Runtime mode toggle */} + +
+ + {/* Right side: send / stop button */} +
+ {isPreparingWorktree ? ( + Preparing worktree... + ) : null} + {activePendingProgress ? ( +
+ {activePendingProgress.questionIndex > 0 ? ( + + ) : null} - ) : ( -
+
+ ) : phase === "running" ? ( + + ) : pendingUserInputs.length === 0 ? ( + showPlanFollowUpPrompt ? ( + prompt.trim().length > 0 ? ( - - - } + ) : ( +
+ + + + } > - Implement in new thread - - - -
+ +
+ + void onImplementPlanInNewThread()} + > + Implement in new thread + + +
+
+ ) + ) : ( + ) - ) : ( - - ) - ) : null} + ) : null} +
-
+ )}
@@ -4000,18 +4017,11 @@ const ProviderHealthBanner = memo(function ProviderHealthBanner({ interface ComposerPendingApprovalPanelProps { approval: PendingApproval; pendingCount: number; - isResponding: boolean; - onRespondToApproval: ( - requestId: ApprovalRequestId, - decision: ProviderApprovalDecision, - ) => Promise; } const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({ approval, pendingCount, - isResponding, - onRespondToApproval, }: ComposerPendingApprovalPanelProps) { const approvalSummary = approval.requestKind === "command" @@ -4029,47 +4039,62 @@ const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel( 1/{pendingCount} ) : null}
-

- {approval.detail} -

-
- - - - -
); }); +interface ComposerPendingApprovalActionsProps { + requestId: ApprovalRequestId; + isResponding: boolean; + onRespondToApproval: ( + requestId: ApprovalRequestId, + decision: ProviderApprovalDecision, + ) => Promise; +} + +const ComposerPendingApprovalActions = memo(function ComposerPendingApprovalActions({ + requestId, + isResponding, + onRespondToApproval, +}: ComposerPendingApprovalActionsProps) { + return ( + <> + + + + + + ); +}); + interface PlanModePanelProps { activePlan: ReturnType; }