| ... | ... | @@ -891,8 +891,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 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) { |
| 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) { |
| 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) { |
| 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) { |
| 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) { |
| 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({ |
| 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( |
| 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 | } |