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) {
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) {
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) {
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) {
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) {
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) {
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({
40004017interface ComposerPendingApprovalPanelProps {
40014018 approval: PendingApproval;
40024019 pendingCount: number;
4003 isResponding: boolean;
4004 onRespondToApproval: (
4005 requestId: ApprovalRequestId,
4006 decision: ProviderApprovalDecision,
4007 ) => Promise<void>;
40084020}
40094021
40104022const 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(
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
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
40734098interface PlanModePanelProps {
40744099 activePlan: ReturnType<typeof deriveActivePlanState>;
40754100}