authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-06 09:01:10-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-06 09:03:16-08:00
logd58ff5de26b9753c3dbd13cb55e532ccd44ba0d2
tree49ebcb6454c89e03310c96ba7797351f56aac9c5
parente28ad181066edece29f98aa717c01fc9f347e33a

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

1 files changed, 293 insertions(+), 268 deletions(-)

apps/web/src/components/ChatView.tsx+293-268
...@@ -891,8 +891,9 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -891,8 +891,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
891 latestTurnSettled &&891 latestTurnSettled &&
892 activeProposedPlan !== null;892 activeProposedPlan !== null;
893 const activePendingApproval = pendingApprovals[0] ?? null;893 const activePendingApproval = pendingApprovals[0] ?? null;
894 const isComposerApprovalState = activePendingApproval !== null;
894 const hasComposerHeader =895 const hasComposerHeader =
895 activePendingApproval !== null ||896 isComposerApprovalState ||
896 pendingUserInputs.length > 0 ||897 pendingUserInputs.length > 0 ||
897 (showPlanFollowUpPrompt && activeProposedPlan !== null);898 (showPlanFollowUpPrompt && activeProposedPlan !== null);
898 useEffect(() => {899 useEffect(() => {
...@@ -3381,8 +3382,6 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3381,8 +3382,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
3381 <ComposerPendingApprovalPanel3382 <ComposerPendingApprovalPanel
3382 approval={activePendingApproval}3383 approval={activePendingApproval}
3383 pendingCount={pendingApprovals.length}3384 pendingCount={pendingApprovals.length}
3384 isResponding={respondingRequestIds.includes(activePendingApproval.requestId)}
3385 onRespondToApproval={onRespondToApproval}
3386 />3385 />
3387 </div>3386 </div>
3388 ) : pendingUserInputs.length > 0 ? (3387 ) : pendingUserInputs.length > 0 ? (
...@@ -3411,7 +3410,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3411,7 +3410,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
3411 hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4",3410 hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4",
3412 )}3411 )}
3413 >3412 >
3414 {composerMenuOpen && (3413 {composerMenuOpen && !isComposerApprovalState && (
3415 <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1">3414 <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1">
3416 <ComposerCommandMenu3415 <ComposerCommandMenu
3417 items={composerMenuItems}3416 items={composerMenuItems}
...@@ -3425,7 +3424,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3425,7 +3424,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
3425 </div>3424 </div>
3426 )}3425 )}
34273426
3428 {pendingUserInputs.length === 0 && composerImages.length > 0 && (3427 {!isComposerApprovalState && pendingUserInputs.length === 0 && composerImages.length > 0 && (
3429 <div className="mb-3 flex flex-wrap gap-2">3428 <div className="mb-3 flex flex-wrap gap-2">
3430 {composerImages.map((image) => (3429 {composerImages.map((image) => (
3431 <div3430 <div
...@@ -3491,13 +3490,21 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3491,13 +3490,21 @@ export default function ChatView({ threadId }: ChatViewProps) {
3491 )}3490 )}
3492 <ComposerPromptEditor3491 <ComposerPromptEditor
3493 ref={composerEditorRef}3492 ref={composerEditorRef}
3494 value={activePendingProgress ? activePendingProgress.customAnswer : prompt}3493 value={
3494 isComposerApprovalState
3495 ? ""
3496 : activePendingProgress
3497 ? activePendingProgress.customAnswer
3498 : prompt
3499 }
3495 cursor={composerCursor}3500 cursor={composerCursor}
3496 onChange={onPromptChange}3501 onChange={onPromptChange}
3497 onCommandKeyDown={onComposerCommandKey}3502 onCommandKeyDown={onComposerCommandKey}
3498 onPaste={onComposerPaste}3503 onPaste={onComposerPaste}
3499 placeholder={3504 placeholder={
3500 activePendingProgress3505 isComposerApprovalState
3506 ? (activePendingApproval?.detail ?? "Resolve this approval request to continue")
3507 : activePendingProgress
3501 ? "Type your own answer, or leave this blank to use the selected option"3508 ? "Type your own answer, or leave this blank to use the selected option"
3502 : showPlanFollowUpPrompt && activeProposedPlan3509 : showPlanFollowUpPrompt && activeProposedPlan
3503 ? "Add feedback to refine the plan, or leave this blank to implement it"3510 ? "Add feedback to refine the plan, or leave this blank to implement it"
...@@ -3505,247 +3512,257 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3505,247 +3512,257 @@ export default function ChatView({ threadId }: ChatViewProps) {
3505 ? "Ask for follow-up changes or attach images"3512 ? "Ask for follow-up changes or attach images"
3506 : "Ask anything, @tag files/folders, or use /model"3513 : "Ask anything, @tag files/folders, or use /model"
3507 }3514 }
3508 disabled={isConnecting}3515 disabled={isConnecting || isComposerApprovalState}
3509 />3516 />
3510 </div>3517 </div>
35113518
3512 {/* Bottom toolbar */}3519 {/* 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">3520 {activePendingApproval ? (
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">3521 <div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3">
3515 {/* Provider/model picker */}3522 <ComposerPendingApprovalActions
3516 <ProviderModelPicker3523 requestId={activePendingApproval.requestId}
3517 provider={selectedProvider}3524 isResponding={respondingRequestIds.includes(activePendingApproval.requestId)}
3518 model={selectedModelForPickerWithCustomFallback}3525 onRespondToApproval={onRespondToApproval}
3519 lockedProvider={lockedProvider}
3520 modelOptionsByProvider={modelOptionsByProvider}
3521 onProviderModelChange={onProviderModelSelect}
3522 />3526 />
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>
3588 </div>3527 </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 */}3540 {selectedProvider === "codex" && selectedEffort != null ? (
3591 <div className="flex shrink-0 items-center gap-2">3541 <>
3592 {isPreparingWorktree ? (3542 <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3593 <span className="text-muted-foreground/70 text-xs">Preparing worktree...</span>3543 <CodexTraitsPicker
3594 ) : null}3544 effort={selectedEffort}
3595 {activePendingProgress ? (3545 fastModeEnabled={selectedCodexFastModeEnabled}
3596 <div className="flex items-center gap-2">3546 options={reasoningOptions}
3597 {activePendingProgress.questionIndex > 0 ? (3547 onEffortChange={onEffortSelect}
3598 <Button3548 onFastModeChange={onCodexFastModeChange}
3599 size="sm"3549 />
3600 variant="outline"3550 </>
3601 className="rounded-full"3551 ) : null}
3602 onClick={onPreviousActivePendingUserInputQuestion}3552
3603 disabled={activePendingIsResponding}3553 {/* Divider */}
3604 >3554 <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3605 Previous3555
3606 </Button>3556 {/* Interaction mode toggle */}
3607 ) : null}3557 <Button
3608 <Button3558 variant="ghost"
3609 type="submit"3559 className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
3610 size="sm"3560 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
3628 type="button"3561 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"3562 onClick={() =>
3630 onClick={() => void onInterrupt()}3563 void handleInteractionModeChange(
3631 aria-label="Stop generation"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 }
3632 >3572 >
3633 <svg3573 <BotIcon />
3634 width="12"3574 <span className="sr-only sm:not-sr-only">
3635 height="12"3575 {interactionMode === "plan" ? "Plan" : "Chat"}
3636 viewBox="0 0 12 12"3576 </span>
3637 fill="currentColor"3577 </Button>
3638 aria-hidden="true"3578
3639 >3579 {/* Divider */}
3640 <rect x="2" y="2" width="8" height="8" rx="1.5" />3580 <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
3641 </svg>3581
3642 </button>3582 {/* Runtime mode toggle */}
3643 ) : pendingUserInputs.length === 0 ? (3583 <Button
3644 showPlanFollowUpPrompt ? (3584 variant="ghost"
3645 prompt.trim().length > 0 ? (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}
3646 <Button3624 <Button
3647 type="submit"3625 type="submit"
3648 size="sm"3626 size="sm"
3649 className="h-9 rounded-full px-4 sm:h-8"3627 className="rounded-full px-4"
3650 disabled={isSendBusy || isConnecting}3628 disabled={
3629 activePendingIsResponding ||
3630 (activePendingProgress.isLastQuestion
3631 ? !activePendingResolvedAnswers
3632 : !activePendingProgress.canAdvance)
3633 }
3651 >3634 >
3652 {isConnecting || isSendBusy ? "Sending..." : "Refine"}3635 {activePendingIsResponding
3636 ? "Submitting..."
3637 : activePendingProgress.isLastQuestion
3638 ? "Submit answers"
3639 : "Next question"}
3653 </Button>3640 </Button>
3654 ) : (3641 </div>
3655 <div className="flex items-center">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 ? (
3656 <Button3662 <Button
3657 type="submit"3663 type="submit"
3658 size="sm"3664 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"
3660 disabled={isSendBusy || isConnecting}3666 disabled={isSendBusy || isConnecting}
3661 >3667 >
3662 {isConnecting || isSendBusy ? "Sending..." : "Implement"}3668 {isConnecting || isSendBusy ? "Sending..." : "Refine"}
3663 </Button>3669 </Button>
3664 <Menu>3670 ) : (
3665 <MenuTrigger3671 <div className="flex items-center">
3666 render={3672 <Button
3667 <Button3673 type="submit"
3668 size="sm"3674 size="sm"
3669 variant="default"3675 className="h-9 rounded-l-full rounded-r-none px-4 sm:h-8"
3670 className="h-9 rounded-l-none rounded-r-full border-l-white/12 px-2 sm:h-8"3676 disabled={isSendBusy || isConnecting}
3671 aria-label="Implementation actions"
3672 disabled={isSendBusy || isConnecting}
3673 />
3674 }
3675 >3677 >
3676 <ChevronDownIcon className="size-3.5" />3678 {isConnecting || isSendBusy ? "Sending..." : "Implement"}
3677 </MenuTrigger>3679 </Button>
3678 <MenuPopup align="end" side="top">3680 <Menu>
3679 <MenuItem3681 <MenuTrigger
3680 disabled={isSendBusy || isConnecting}3682 render={
3681 onClick={() => void onImplementPlanInNewThread()}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 }
3682 >3691 >
3683 Implement in new thread3692 <ChevronDownIcon className="size-3.5" />
3684 </MenuItem>3693 </MenuTrigger>
3685 </MenuPopup>3694 <MenuPopup align="end" side="top">
3686 </Menu>3695 <MenuItem
3687 </div>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>
3688 )3761 )
3689 ) : (3762 ) : null}
3690 <button3763 </div>
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}
3747 </div>3764 </div>
3748 </div>3765 )}
3749 </div>3766 </div>
3750 </form>3767 </form>
3751 </div>3768 </div>
...@@ -4000,18 +4017,11 @@ const ProviderHealthBanner = memo(function ProviderHealthBanner({...@@ -4000,18 +4017,11 @@ const ProviderHealthBanner = memo(function ProviderHealthBanner({
4000interface ComposerPendingApprovalPanelProps {4017interface ComposerPendingApprovalPanelProps {
4001 approval: PendingApproval;4018 approval: PendingApproval;
4002 pendingCount: number;4019 pendingCount: number;
4003 isResponding: boolean;
4004 onRespondToApproval: (
4005 requestId: ApprovalRequestId,
4006 decision: ProviderApprovalDecision,
4007 ) => Promise<void>;
4008}4020}
40094021
4010const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({4022const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({
4011 approval,4023 approval,
4012 pendingCount,4024 pendingCount,
4013 isResponding,
4014 onRespondToApproval,
4015}: ComposerPendingApprovalPanelProps) {4025}: ComposerPendingApprovalPanelProps) {
4016 const approvalSummary =4026 const approvalSummary =
4017 approval.requestKind === "command"4027 approval.requestKind === "command"
...@@ -4029,47 +4039,62 @@ const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel(...@@ -4029,47 +4039,62 @@ const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel(
4029 <span className="text-xs text-muted-foreground">1/{pendingCount}</span>4039 <span className="text-xs text-muted-foreground">1/{pendingCount}</span>
4030 ) : null}4040 ) : null}
4031 </div>4041 </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>
4069 </div>4042 </div>
4070 );4043 );
4071});4044});
40724045
4046interface ComposerPendingApprovalActionsProps {
4047 requestId: ApprovalRequestId;
4048 isResponding: boolean;
4049 onRespondToApproval: (
4050 requestId: ApprovalRequestId,
4051 decision: ProviderApprovalDecision,
4052 ) => Promise<void>;
4053}
4054
4055const 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
4073interface PlanModePanelProps {4098interface PlanModePanelProps {
4074 activePlan: ReturnType<typeof deriveActivePlanState>;4099 activePlan: ReturnType<typeof deriveActivePlanState>;
4075}4100}