History

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

Julius Marminge committed 8 months ago

Commit d58ff5d1 parent e28ad181 file · +293 −268Browse files

Unified · Split

apps/web/src/components/ChatView.tsx+293−268
@@ -891,8 +891,9 @@ export default function ChatView({ threadId }: ChatViewProps) { · 890 unmodified lines
891891 latestTurnSettled &&
892892 activeProposedPlan !== null;
893893 const activePendingApproval = pendingApprovals[0] ?? null;
894+ const isComposerApprovalState = activePendingApproval !== null;
894895 const hasComposerHeader =
895− activePendingApproval !== null ||
896+ isComposerApprovalState ||
896897 pendingUserInputs.length > 0 ||
897898 (showPlanFollowUpPrompt && activeProposedPlan !== null);
898899 useEffect(() => {
@@ -3381,8 +3382,6 @@ export default function ChatView({ threadId }: ChatViewProps) { · 2482 unmodified lines
33813382 <ComposerPendingApprovalPanel
33823383 approval={activePendingApproval}
33833384 pendingCount={pendingApprovals.length}
3384− isResponding={respondingRequestIds.includes(activePendingApproval.requestId)}
3385− onRespondToApproval={onRespondToApproval}
33863385 />
33873386 </div>
33883387 ) : pendingUserInputs.length > 0 ? (
@@ -3411,7 +3410,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 22 unmodified lines
34113410 hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4",
34123411 )}
34133412 >
3414− {composerMenuOpen && (
3413+ {composerMenuOpen && !isComposerApprovalState && (
34153414 <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1">
34163415 <ComposerCommandMenu
34173416 items={composerMenuItems}
@@ -3425,7 +3424,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 7 unmodified lines
34253424 </div>
34263425 )}
34273426
3428− {pendingUserInputs.length === 0 && composerImages.length > 0 && (
3427+ {!isComposerApprovalState && pendingUserInputs.length === 0 && composerImages.length > 0 && (
34293428 <div className="mb-3 flex flex-wrap gap-2">
34303429 {composerImages.map((image) => (
34313430 <div
@@ -3491,13 +3490,21 @@ export default function ChatView({ threadId }: ChatViewProps) { · 59 unmodified lines
34913490 )}
34923491 <ComposerPromptEditor
34933492 ref={composerEditorRef}
3494− value={activePendingProgress ? activePendingProgress.customAnswer : prompt}
3493+ value={
3494+ isComposerApprovalState
3495+ ? ""
3496+ : activePendingProgress
3497+ ? activePendingProgress.customAnswer
3498+ : prompt
3499+ }
34953500 cursor={composerCursor}
34963501 onChange={onPromptChange}
34973502 onCommandKeyDown={onComposerCommandKey}
34983503 onPaste={onComposerPaste}
34993504 placeholder={
3500− activePendingProgress
3505+ isComposerApprovalState
3506+ ? (activePendingApproval?.detail ?? "Resolve this approval request to continue")
3507+ : activePendingProgress
35013508 ? "Type your own answer, or leave this blank to use the selected option"
35023509 : showPlanFollowUpPrompt && activeProposedPlan
35033510 ? "Add feedback to refine the plan, or leave this blank to implement it"
@@ -3505,247 +3512,257 @@ export default function ChatView({ threadId }: ChatViewProps) { · 1 unmodified line
35053512 ? "Ask for follow-up changes or attach images"
35063513 : "Ask anything, @tag files/folders, or use /model"
35073514 }
3508− disabled={isConnecting}
3515+ disabled={isConnecting || isComposerApprovalState}
35093516 />
35103517 </div>
35113518
35123519 {/* Bottom toolbar */}
3513− <div className="flex flex-wrap items-center justify-between gap-2 px-2.5 pb-2.5 sm:flex-nowrap sm:gap-0 sm:px-3 sm:pb-3">
3514− <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:min-w-max sm:overflow-visible">
3515− {/* Provider/model picker */}
3516− <ProviderModelPicker
3517− provider={selectedProvider}
3518− model={selectedModelForPickerWithCustomFallback}
3519− lockedProvider={lockedProvider}
3520− modelOptionsByProvider={modelOptionsByProvider}
3521− onProviderModelChange={onProviderModelSelect}
3520+ {activePendingApproval ? (
3521+ <div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3">
3522+ <ComposerPendingApprovalActions
3523+ requestId={activePendingApproval.requestId}
3524+ isResponding={respondingRequestIds.includes(activePendingApproval.requestId)}
3525+ onRespondToApproval={onRespondToApproval}
35223526 />
3523−
3524− {selectedProvider === "codex" && selectedEffort != null ? (
3525− <>
3526− <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3527− <CodexTraitsPicker
3528− effort={selectedEffort}
3529− fastModeEnabled={selectedCodexFastModeEnabled}
3530− options={reasoningOptions}
3531− onEffortChange={onEffortSelect}
3532− onFastModeChange={onCodexFastModeChange}
3533− />
3534− </>
3535− ) : null}
3536−
3537− {/* Divider */}
3538− <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3539−
3540− {/* Interaction mode toggle */}
3541− <Button
3542− variant="ghost"
3543− className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
3544− size="sm"
3545− type="button"
3546− onClick={() =>
3547− void handleInteractionModeChange(
3548− interactionMode === "plan" ? "default" : "plan",
3549− )
3550− }
3551− title={
3552− interactionMode === "plan"
3553− ? "Plan mode — click to return to normal chat mode"
3554− : "Default mode — click to enter plan mode"
3555− }
3556− >
3557− <BotIcon />
3558− <span className="sr-only sm:not-sr-only">
3559− {interactionMode === "plan" ? "Plan" : "Chat"}
3560− </span>
3561− </Button>
3562−
3563− {/* Divider */}
3564− <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3565−
3566− {/* Runtime mode toggle */}
3567− <Button
3568− variant="ghost"
3569− className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
3570− size="sm"
3571− type="button"
3572− onClick={() =>
3573− void handleRuntimeModeChange(
3574− runtimeMode === "full-access" ? "approval-required" : "full-access",
3575− )
3576− }
3577− title={
3578− runtimeMode === "full-access"
3579− ? "Full access — click to require approvals"
3580− : "Approval required — click for full access"
3581− }
3582− >
3583− {runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />}
3584− <span className="sr-only sm:not-sr-only">
3585− {runtimeMode === "full-access" ? "Full access" : "Supervised"}
3586− </span>
3587− </Button>
35883527 </div>
3528+ ) : (
3529+ <div className="flex flex-wrap items-center justify-between gap-2 px-2.5 pb-2.5 sm:flex-nowrap sm:gap-0 sm:px-3 sm:pb-3">
3530+ <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:min-w-max sm:overflow-visible">
3531+ {/* Provider/model picker */}
3532+ <ProviderModelPicker
3533+ provider={selectedProvider}
3534+ model={selectedModelForPickerWithCustomFallback}
3535+ lockedProvider={lockedProvider}
3536+ modelOptionsByProvider={modelOptionsByProvider}
3537+ onProviderModelChange={onProviderModelSelect}
3538+ />
35893539
3590− {/* Right side: send / stop button */}
3591− <div className="flex shrink-0 items-center gap-2">
3592− {isPreparingWorktree ? (
3593− <span className="text-muted-foreground/70 text-xs">Preparing worktree...</span>
3594− ) : null}
3595− {activePendingProgress ? (
3596− <div className="flex items-center gap-2">
3597− {activePendingProgress.questionIndex > 0 ? (
3598− <Button
3599− size="sm"
3600− variant="outline"
3601− className="rounded-full"
3602− onClick={onPreviousActivePendingUserInputQuestion}
3603− disabled={activePendingIsResponding}
3604− >
3605− Previous
3606− </Button>
3607− ) : null}
3608− <Button
3609− type="submit"
3610− size="sm"
3611− className="rounded-full px-4"
3612− disabled={
3613− activePendingIsResponding ||
3614− (activePendingProgress.isLastQuestion
3615− ? !activePendingResolvedAnswers
3616− : !activePendingProgress.canAdvance)
3617− }
3618− >
3619− {activePendingIsResponding
3620− ? "Submitting..."
3621− : activePendingProgress.isLastQuestion
3622− ? "Submit answers"
3623− : "Next question"}
3624− </Button>
3625− </div>
3626− ) : phase === "running" ? (
3627− <button
3540+ {selectedProvider === "codex" && selectedEffort != null ? (
3541+ <>
3542+ <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3543+ <CodexTraitsPicker
3544+ effort={selectedEffort}
3545+ fastModeEnabled={selectedCodexFastModeEnabled}
3546+ options={reasoningOptions}
3547+ onEffortChange={onEffortSelect}
3548+ onFastModeChange={onCodexFastModeChange}
3549+ />
3550+ </>
3551+ ) : null}
3552+
3553+ {/* Divider */}
3554+ <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3555+
3556+ {/* Interaction mode toggle */}
3557+ <Button
3558+ variant="ghost"
3559+ className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
3560+ size="sm"
36283561 type="button"
3629− className="flex size-8 items-center justify-center rounded-full bg-rose-500/90 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105 sm:h-8 sm:w-8"
3630− onClick={() => void onInterrupt()}
3631− aria-label="Stop generation"
3562+ onClick={() =>
3563+ void handleInteractionModeChange(
3564+ interactionMode === "plan" ? "default" : "plan",
3565+ )
3566+ }
3567+ title={
3568+ interactionMode === "plan"
3569+ ? "Plan mode — click to return to normal chat mode"
3570+ : "Default mode — click to enter plan mode"
3571+ }
36323572 >
3633− <svg
3634− width="12"
3635− height="12"
3636− viewBox="0 0 12 12"
3637− fill="currentColor"
3638− aria-hidden="true"
3639− >
3640− <rect x="2" y="2" width="8" height="8" rx="1.5" />
3641− </svg>
3642− </button>
3643− ) : pendingUserInputs.length === 0 ? (
3644− showPlanFollowUpPrompt ? (
3645− prompt.trim().length > 0 ? (
3573+ <BotIcon />
3574+ <span className="sr-only sm:not-sr-only">
3575+ {interactionMode === "plan" ? "Plan" : "Chat"}
3576+ </span>
3577+ </Button>
3578+
3579+ {/* Divider */}
3580+ <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3581+
3582+ {/* Runtime mode toggle */}
3583+ <Button
3584+ variant="ghost"
3585+ className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
3586+ size="sm"
3587+ type="button"
3588+ onClick={() =>
3589+ void handleRuntimeModeChange(
3590+ runtimeMode === "full-access" ? "approval-required" : "full-access",
3591+ )
3592+ }
3593+ title={
3594+ runtimeMode === "full-access"
3595+ ? "Full access — click to require approvals"
3596+ : "Approval required — click for full access"
3597+ }
3598+ >
3599+ {runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />}
3600+ <span className="sr-only sm:not-sr-only">
3601+ {runtimeMode === "full-access" ? "Full access" : "Supervised"}
3602+ </span>
3603+ </Button>
3604+ </div>
3605+
3606+ {/* Right side: send / stop button */}
3607+ <div className="flex shrink-0 items-center gap-2">
3608+ {isPreparingWorktree ? (
3609+ <span className="text-muted-foreground/70 text-xs">Preparing worktree...</span>
3610+ ) : null}
3611+ {activePendingProgress ? (
3612+ <div className="flex items-center gap-2">
3613+ {activePendingProgress.questionIndex > 0 ? (
3614+ <Button
3615+ size="sm"
3616+ variant="outline"
3617+ className="rounded-full"
3618+ onClick={onPreviousActivePendingUserInputQuestion}
3619+ disabled={activePendingIsResponding}
3620+ >
3621+ Previous
3622+ </Button>
3623+ ) : null}
36463624 <Button
36473625 type="submit"
36483626 size="sm"
3649− className="h-9 rounded-full px-4 sm:h-8"
3650− disabled={isSendBusy || isConnecting}
3627+ className="rounded-full px-4"
3628+ disabled={
3629+ activePendingIsResponding ||
3630+ (activePendingProgress.isLastQuestion
3631+ ? !activePendingResolvedAnswers
3632+ : !activePendingProgress.canAdvance)
3633+ }
36513634 >
3652− {isConnecting || isSendBusy ? "Sending..." : "Refine"}
3635+ {activePendingIsResponding
3636+ ? "Submitting..."
3637+ : activePendingProgress.isLastQuestion
3638+ ? "Submit answers"
3639+ : "Next question"}
36533640 </Button>
3654− ) : (
3655− <div className="flex items-center">
3641+ </div>
3642+ ) : phase === "running" ? (
3643+ <button
3644+ type="button"
3645+ className="flex size-8 items-center justify-center rounded-full bg-rose-500/90 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105 sm:h-8 sm:w-8"
3646+ onClick={() => void onInterrupt()}
3647+ aria-label="Stop generation"
3648+ >
3649+ <svg
3650+ width="12"
3651+ height="12"
3652+ viewBox="0 0 12 12"
3653+ fill="currentColor"
3654+ aria-hidden="true"
3655+ >
3656+ <rect x="2" y="2" width="8" height="8" rx="1.5" />
3657+ </svg>
3658+ </button>
3659+ ) : pendingUserInputs.length === 0 ? (
3660+ showPlanFollowUpPrompt ? (
3661+ prompt.trim().length > 0 ? (
36563662 <Button
36573663 type="submit"
36583664 size="sm"
3659− className="h-9 rounded-l-full rounded-r-none px-4 sm:h-8"
3665+ className="h-9 rounded-full px-4 sm:h-8"
36603666 disabled={isSendBusy || isConnecting}
36613667 >
3662− {isConnecting || isSendBusy ? "Sending..." : "Implement"}
3668+ {isConnecting || isSendBusy ? "Sending..." : "Refine"}
36633669 </Button>
3664− <Menu>
3665− <MenuTrigger
3666− render={
3667− <Button
3668− size="sm"
3669− variant="default"
3670− className="h-9 rounded-l-none rounded-r-full border-l-white/12 px-2 sm:h-8"
3671− aria-label="Implementation actions"
3672− disabled={isSendBusy || isConnecting}
3673− />
3674− }
3670+ ) : (
3671+ <div className="flex items-center">
3672+ <Button
3673+ type="submit"
3674+ size="sm"
3675+ className="h-9 rounded-l-full rounded-r-none px-4 sm:h-8"
3676+ disabled={isSendBusy || isConnecting}
36753677 >
3676− <ChevronDownIcon className="size-3.5" />
3677− </MenuTrigger>
3678− <MenuPopup align="end" side="top">
3679− <MenuItem
3680− disabled={isSendBusy || isConnecting}
3681− onClick={() => void onImplementPlanInNewThread()}
3678+ {isConnecting || isSendBusy ? "Sending..." : "Implement"}
3679+ </Button>
3680+ <Menu>
3681+ <MenuTrigger
3682+ render={
3683+ <Button
3684+ size="sm"
3685+ variant="default"
3686+ className="h-9 rounded-l-none rounded-r-full border-l-white/12 px-2 sm:h-8"
3687+ aria-label="Implementation actions"
3688+ disabled={isSendBusy || isConnecting}
3689+ />
3690+ }
36823691 >
3683− Implement in new thread
3684− </MenuItem>
3685− </MenuPopup>
3686− </Menu>
3687− </div>
3692+ <ChevronDownIcon className="size-3.5" />
3693+ </MenuTrigger>
3694+ <MenuPopup align="end" side="top">
3695+ <MenuItem
3696+ disabled={isSendBusy || isConnecting}
3697+ onClick={() => void onImplementPlanInNewThread()}
3698+ >
3699+ Implement in new thread
3700+ </MenuItem>
3701+ </MenuPopup>
3702+ </Menu>
3703+ </div>
3704+ )
3705+ ) : (
3706+ <button
3707+ type="submit"
3708+ className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/90 text-primary-foreground transition-all duration-150 hover:bg-primary hover:scale-105 disabled:opacity-30 disabled:hover:scale-100 sm:h-8 sm:w-8"
3709+ disabled={
3710+ isSendBusy ||
3711+ isConnecting ||
3712+ (!prompt.trim() && composerImages.length === 0)
3713+ }
3714+ aria-label={
3715+ isConnecting
3716+ ? "Connecting"
3717+ : isPreparingWorktree
3718+ ? "Preparing worktree"
3719+ : isSendBusy
3720+ ? "Sending"
3721+ : "Send message"
3722+ }
3723+ >
3724+ {isConnecting || isSendBusy ? (
3725+ <svg
3726+ width="14"
3727+ height="14"
3728+ viewBox="0 0 14 14"
3729+ fill="none"
3730+ className="animate-spin"
3731+ aria-hidden="true"
3732+ >
3733+ <circle
3734+ cx="7"
3735+ cy="7"
3736+ r="5.5"
3737+ stroke="currentColor"
3738+ strokeWidth="1.5"
3739+ strokeLinecap="round"
3740+ strokeDasharray="20 12"
3741+ />
3742+ </svg>
3743+ ) : (
3744+ <svg
3745+ width="14"
3746+ height="14"
3747+ viewBox="0 0 14 14"
3748+ fill="none"
3749+ aria-hidden="true"
3750+ >
3751+ <path
3752+ d="M7 11.5V2.5M7 2.5L3 6.5M7 2.5L11 6.5"
3753+ stroke="currentColor"
3754+ strokeWidth="1.8"
3755+ strokeLinecap="round"
3756+ strokeLinejoin="round"
3757+ />
3758+ </svg>
3759+ )}
3760+ </button>
36883761 )
3689− ) : (
3690− <button
3691− type="submit"
3692− className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/90 text-primary-foreground transition-all duration-150 hover:bg-primary hover:scale-105 disabled:opacity-30 disabled:hover:scale-100 sm:h-8 sm:w-8"
3693− disabled={
3694− isSendBusy ||
3695− isConnecting ||
3696− (!prompt.trim() && composerImages.length === 0)
3697− }
3698− aria-label={
3699− isConnecting
3700− ? "Connecting"
3701− : isPreparingWorktree
3702− ? "Preparing worktree"
3703− : isSendBusy
3704− ? "Sending"
3705− : "Send message"
3706− }
3707− >
3708− {isConnecting || isSendBusy ? (
3709− <svg
3710− width="14"
3711− height="14"
3712− viewBox="0 0 14 14"
3713− fill="none"
3714− className="animate-spin"
3715− aria-hidden="true"
3716− >
3717− <circle
3718− cx="7"
3719− cy="7"
3720− r="5.5"
3721− stroke="currentColor"
3722− strokeWidth="1.5"
3723− strokeLinecap="round"
3724− strokeDasharray="20 12"
3725− />
3726− </svg>
3727− ) : (
3728− <svg
3729− width="14"
3730− height="14"
3731− viewBox="0 0 14 14"
3732− fill="none"
3733− aria-hidden="true"
3734− >
3735− <path
3736− d="M7 11.5V2.5M7 2.5L3 6.5M7 2.5L11 6.5"
3737− stroke="currentColor"
3738− strokeWidth="1.8"
3739− strokeLinecap="round"
3740− strokeLinejoin="round"
3741− />
3742− </svg>
3743− )}
3744− </button>
3745− )
3746− ) : null}
3762+ ) : null}
3763+ </div>
37473764 </div>
3748− </div>
3765+ )}
37493766 </div>
37503767 </form>
37513768 </div>
@@ -4000,18 +4017,11 @@ const ProviderHealthBanner = memo(function ProviderHealthBanner({ · 248 unmodified lines
40004017 interface ComposerPendingApprovalPanelProps {
40014018 approval: PendingApproval;
40024019 pendingCount: number;
4003− isResponding: boolean;
4004− onRespondToApproval: (
4005− requestId: ApprovalRequestId,
4006− decision: ProviderApprovalDecision,
4007− ) => Promise<void>;
40084020 }
40094021
40104022 const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({
40114023 approval,
40124024 pendingCount,
4013− isResponding,
4014− onRespondToApproval,
40154025 }: ComposerPendingApprovalPanelProps) {
40164026 const approvalSummary =
40174027 approval.requestKind === "command"
@@ -4029,47 +4039,62 @@ const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel( · 11 unmodified lines
40294039 <span className="text-xs text-muted-foreground">1/{pendingCount}</span>
40304040 ) : null}
40314041 </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>
40694042 </div>
40704043 );
40714044 });
40724045
4046+interface ComposerPendingApprovalActionsProps {
4047+ requestId: ApprovalRequestId;
4048+ isResponding: boolean;
4049+ onRespondToApproval: (
4050+ requestId: ApprovalRequestId,
4051+ decision: ProviderApprovalDecision,
4052+ ) => Promise<void>;
4053+}
4054+
4055+const ComposerPendingApprovalActions = memo(function ComposerPendingApprovalActions({
4056+ requestId,
4057+ isResponding,
4058+ onRespondToApproval,
4059+}: ComposerPendingApprovalActionsProps) {
4060+ return (
4061+ <>
4062+ <Button
4063+ size="sm"
4064+ variant="ghost"
4065+ disabled={isResponding}
4066+ onClick={() => void onRespondToApproval(requestId, "cancel")}
4067+ >
4068+ Cancel turn
4069+ </Button>
4070+ <Button
4071+ size="sm"
4072+ variant="destructive-outline"
4073+ disabled={isResponding}
4074+ onClick={() => void onRespondToApproval(requestId, "decline")}
4075+ >
4076+ Decline
4077+ </Button>
4078+ <Button
4079+ size="sm"
4080+ variant="outline"
4081+ disabled={isResponding}
4082+ onClick={() => void onRespondToApproval(requestId, "acceptForSession")}
4083+ >
4084+ Always allow this session
4085+ </Button>
4086+ <Button
4087+ size="sm"
4088+ variant="default"
4089+ disabled={isResponding}
4090+ onClick={() => void onRespondToApproval(requestId, "accept")}
4091+ >
4092+ Approve once
4093+ </Button>
4094+ </>
4095+ );
4096+});
4097+
40734098 interface PlanModePanelProps {
40744099 activePlan: ReturnType<typeof deriveActivePlanState>;
40754100 }