- 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
Unified · Split
| @@ -891,8 +891,9 @@ export default function ChatView({ threadId }: ChatViewProps) { · 890 unmodified lines | ||
| 891 | 891 | latestTurnSettled && |
| 892 | 892 | activeProposedPlan !== null; |
| 893 | 893 | const activePendingApproval = pendingApprovals[0] ?? null; |
| 894 | + const isComposerApprovalState = activePendingApproval !== null; | |
| 894 | 895 | const hasComposerHeader = |
| 895 | − activePendingApproval !== null || | |
| 896 | + isComposerApprovalState || | |
| 896 | 897 | pendingUserInputs.length > 0 || |
| 897 | 898 | (showPlanFollowUpPrompt && activeProposedPlan !== null); |
| 898 | 899 | useEffect(() => { |
| @@ -3381,8 +3382,6 @@ export default function ChatView({ threadId }: ChatViewProps) { · 2482 unmodified lines | ||
| 3381 | 3382 | <ComposerPendingApprovalPanel |
| 3382 | 3383 | approval={activePendingApproval} |
| 3383 | 3384 | pendingCount={pendingApprovals.length} |
| 3384 | − isResponding={respondingRequestIds.includes(activePendingApproval.requestId)} | |
| 3385 | − onRespondToApproval={onRespondToApproval} | |
| 3386 | 3385 | /> |
| 3387 | 3386 | </div> |
| 3388 | 3387 | ) : pendingUserInputs.length > 0 ? ( |
| @@ -3411,7 +3410,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 22 unmodified lines | ||
| 3411 | 3410 | hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4", |
| 3412 | 3411 | )} |
| 3413 | 3412 | > |
| 3414 | − {composerMenuOpen && ( | |
| 3413 | + {composerMenuOpen && !isComposerApprovalState && ( | |
| 3415 | 3414 | <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1"> |
| 3416 | 3415 | <ComposerCommandMenu |
| 3417 | 3416 | items={composerMenuItems} |
| @@ -3425,7 +3424,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 7 unmodified lines | ||
| 3425 | 3424 | </div> |
| 3426 | 3425 | )} |
| 3427 | 3426 | |
| 3428 | − {pendingUserInputs.length === 0 && composerImages.length > 0 && ( | |
| 3427 | + {!isComposerApprovalState && pendingUserInputs.length === 0 && composerImages.length > 0 && ( | |
| 3429 | 3428 | <div className="mb-3 flex flex-wrap gap-2"> |
| 3430 | 3429 | {composerImages.map((image) => ( |
| 3431 | 3430 | <div |
| @@ -3491,13 +3490,21 @@ export default function ChatView({ threadId }: ChatViewProps) { · 59 unmodified lines | ||
| 3491 | 3490 | )} |
| 3492 | 3491 | <ComposerPromptEditor |
| 3493 | 3492 | ref={composerEditorRef} |
| 3494 | − value={activePendingProgress ? activePendingProgress.customAnswer : prompt} | |
| 3493 | + value={ | |
| 3494 | + isComposerApprovalState | |
| 3495 | + ? "" | |
| 3496 | + : activePendingProgress | |
| 3497 | + ? activePendingProgress.customAnswer | |
| 3498 | + : prompt | |
| 3499 | + } | |
| 3495 | 3500 | cursor={composerCursor} |
| 3496 | 3501 | onChange={onPromptChange} |
| 3497 | 3502 | onCommandKeyDown={onComposerCommandKey} |
| 3498 | 3503 | onPaste={onComposerPaste} |
| 3499 | 3504 | placeholder={ |
| 3500 | − activePendingProgress | |
| 3505 | + isComposerApprovalState | |
| 3506 | + ? (activePendingApproval?.detail ?? "Resolve this approval request to continue") | |
| 3507 | + : activePendingProgress | |
| 3501 | 3508 | ? "Type your own answer, or leave this blank to use the selected option" |
| 3502 | 3509 | : showPlanFollowUpPrompt && activeProposedPlan |
| 3503 | 3510 | ? "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 | ||
| 3505 | 3512 | ? "Ask for follow-up changes or attach images" |
| 3506 | 3513 | : "Ask anything, @tag files/folders, or use /model" |
| 3507 | 3514 | } |
| 3508 | − disabled={isConnecting} | |
| 3515 | + disabled={isConnecting || isComposerApprovalState} | |
| 3509 | 3516 | /> |
| 3510 | 3517 | </div> |
| 3511 | 3518 | |
| 3512 | 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"> | |
| 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} | |
| 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 | 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 | + /> | |
| 3589 | 3539 | |
| 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" | |
| 3628 | 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" | |
| 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 | + } | |
| 3632 | 3572 | > |
| 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} | |
| 3646 | 3624 | <Button |
| 3647 | 3625 | type="submit" |
| 3648 | 3626 | 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 | + } | |
| 3651 | 3634 | > |
| 3652 | − {isConnecting || isSendBusy ? "Sending..." : "Refine"} | |
| 3635 | + {activePendingIsResponding | |
| 3636 | + ? "Submitting..." | |
| 3637 | + : activePendingProgress.isLastQuestion | |
| 3638 | + ? "Submit answers" | |
| 3639 | + : "Next question"} | |
| 3653 | 3640 | </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 ? ( | |
| 3656 | 3662 | <Button |
| 3657 | 3663 | type="submit" |
| 3658 | 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 | 3666 | disabled={isSendBusy || isConnecting} |
| 3661 | 3667 | > |
| 3662 | − {isConnecting || isSendBusy ? "Sending..." : "Implement"} | |
| 3668 | + {isConnecting || isSendBusy ? "Sending..." : "Refine"} | |
| 3663 | 3669 | </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} | |
| 3675 | 3677 | > |
| 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 | + } | |
| 3682 | 3691 | > |
| 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> | |
| 3688 | 3761 | ) |
| 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> | |
| 3747 | 3764 | </div> |
| 3748 | − </div> | |
| 3765 | + )} | |
| 3749 | 3766 | </div> |
| 3750 | 3767 | </form> |
| 3751 | 3768 | </div> |
| @@ -4000,18 +4017,11 @@ const ProviderHealthBanner = memo(function ProviderHealthBanner({ · 248 unmodified lines | ||
| 4000 | 4017 | interface ComposerPendingApprovalPanelProps { |
| 4001 | 4018 | approval: PendingApproval; |
| 4002 | 4019 | pendingCount: number; |
| 4003 | − isResponding: boolean; | |
| 4004 | − onRespondToApproval: ( | |
| 4005 | − requestId: ApprovalRequestId, | |
| 4006 | − decision: ProviderApprovalDecision, | |
| 4007 | − ) => Promise<void>; | |
| 4008 | 4020 | } |
| 4009 | 4021 | |
| 4010 | 4022 | const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({ |
| 4011 | 4023 | approval, |
| 4012 | 4024 | pendingCount, |
| 4013 | − isResponding, | |
| 4014 | − onRespondToApproval, | |
| 4015 | 4025 | }: ComposerPendingApprovalPanelProps) { |
| 4016 | 4026 | const approvalSummary = |
| 4017 | 4027 | approval.requestKind === "command" |
| @@ -4029,47 +4039,62 @@ const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel( · 11 unmodified lines | ||
| 4029 | 4039 | <span className="text-xs text-muted-foreground">1/{pendingCount}</span> |
| 4030 | 4040 | ) : null} |
| 4031 | 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 | 4042 | </div> |
| 4070 | 4043 | ); |
| 4071 | 4044 | }); |
| 4072 | 4045 | |
| 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 | + | |
| 4073 | 4098 | interface PlanModePanelProps { |
| 4074 | 4099 | activePlan: ReturnType<typeof deriveActivePlanState>; |
| 4075 | 4100 | } |