| ... | @@ -111,6 +111,7 @@ import type { PendingApproval, PendingUserInput } from "../../session-logic"; | ... | @@ -111,6 +111,7 @@ import type { PendingApproval, PendingUserInput } from "../../session-logic"; |
| 111 | import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow"; | 111 | import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow"; |
| 112 | import { formatProviderSkillDisplayName } from "../../providerSkillPresentation"; | 112 | import { formatProviderSkillDisplayName } from "../../providerSkillPresentation"; |
| 113 | import { searchProviderSkills } from "../../providerSkillSearch"; | 113 | import { searchProviderSkills } from "../../providerSkillSearch"; |
| | 114 | import { useMediaQuery } from "../../hooks/useMediaQuery"; |
| 114 | | 115 | |
| 115 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; | 116 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; |
| 116 | | 117 | |
| ... | @@ -138,6 +139,13 @@ const runtimeModeConfig: Record< | ... | @@ -138,6 +139,13 @@ const runtimeModeConfig: Record< |
| 138 | const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; | 139 | const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; |
| 139 | const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; | 140 | const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; |
| 140 | const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = []; | 141 | const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = []; |
| | 142 | const COMPOSER_FLOATING_LAYER_SELECTOR = [ |
| | 143 | '[data-slot="popover-popup"]', |
| | 144 | '[data-slot="menu-popup"]', |
| | 145 | '[data-slot="select-popup"]', |
| | 146 | '[data-slot="combobox-popup"]', |
| | 147 | '[data-slot="autocomplete-popup"]', |
| | 148 | ].join(","); |
| 141 | | 149 | |
| 142 | const extendReplacementRangeForTrailingSpace = ( | 150 | const extendReplacementRangeForTrailingSpace = ( |
| 143 | text: string, | 151 | text: string, |
| ... | @@ -167,6 +175,10 @@ const terminalContextIdListsEqual = ( | ... | @@ -167,6 +175,10 @@ const terminalContextIdListsEqual = ( |
| 167 | ): boolean => | 175 | ): boolean => |
| 168 | contexts.length === ids.length && contexts.every((context, index) => context.id === ids[index]); | 176 | contexts.length === ids.length && contexts.every((context, index) => context.id === ids[index]); |
| 169 | | 177 | |
| | 178 | function isInsideComposerFloatingLayer(element: Element): boolean { |
| | 179 | return element.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null; |
| | 180 | } |
| | 181 | |
| 170 | const ComposerFooterModeControls = memo(function ComposerFooterModeControls(props: { | 182 | const ComposerFooterModeControls = memo(function ComposerFooterModeControls(props: { |
| 171 | showInteractionModeToggle: boolean; | 183 | showInteractionModeToggle: boolean; |
| 172 | interactionMode: ProviderInteractionMode; | 184 | interactionMode: ProviderInteractionMode; |
| ... | @@ -290,6 +302,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( | ... | @@ -290,6 +302,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 290 | isSendBusy: boolean; | 302 | isSendBusy: boolean; |
| 291 | isConnecting: boolean; | 303 | isConnecting: boolean; |
| 292 | hasSendableContent: boolean; | 304 | hasSendableContent: boolean; |
| | 305 | preserveComposerFocusOnPointerDown?: boolean; |
| 293 | onPreviousPendingQuestion: () => void; | 306 | onPreviousPendingQuestion: () => void; |
| 294 | onInterrupt: () => void; | 307 | onInterrupt: () => void; |
| 295 | onImplementPlanInNewThread: () => void; | 308 | onImplementPlanInNewThread: () => void; |
| ... | @@ -310,6 +323,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( | ... | @@ -310,6 +323,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 310 | isConnecting={props.isConnecting} | 323 | isConnecting={props.isConnecting} |
| 311 | isPreparingWorktree={props.isPreparingWorktree} | 324 | isPreparingWorktree={props.isPreparingWorktree} |
| 312 | hasSendableContent={props.hasSendableContent} | 325 | hasSendableContent={props.hasSendableContent} |
| | 326 | preserveComposerFocusOnPointerDown={props.preserveComposerFocusOnPointerDown ?? false} |
| 313 | onPreviousPendingQuestion={props.onPreviousPendingQuestion} | 327 | onPreviousPendingQuestion={props.onPreviousPendingQuestion} |
| 314 | onInterrupt={props.onInterrupt} | 328 | onInterrupt={props.onInterrupt} |
| 315 | onImplementPlanInNewThread={props.onImplementPlanInNewThread} | 329 | onImplementPlanInNewThread={props.onImplementPlanInNewThread} |
| ... | @@ -389,7 +403,7 @@ export interface ChatComposerProps { | ... | @@ -389,7 +403,7 @@ export interface ChatComposerProps { |
| 389 | isLastQuestion: boolean; | 403 | isLastQuestion: boolean; |
| 390 | canAdvance: boolean; | 404 | canAdvance: boolean; |
| 391 | customAnswer: string; | 405 | customAnswer: string; |
| 392 | activeQuestion: { id: string } | null; | 406 | activeQuestion: { id: string; multiSelect?: boolean | undefined } | null; |
| 393 | } | null; | 407 | } | null; |
| 394 | activePendingResolvedAnswers: Record<string, unknown> | null; | 408 | activePendingResolvedAnswers: Record<string, unknown> | null; |
| 395 | activePendingIsResponding: boolean; | 409 | activePendingIsResponding: boolean; |
| ... | @@ -774,17 +788,25 @@ export const ChatComposer = memo( | ... | @@ -774,17 +788,25 @@ export const ChatComposer = memo( |
| 774 | const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); | 788 | const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); |
| 775 | const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false); | 789 | const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false); |
| 776 | const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false); | 790 | const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false); |
| | 791 | const [isComposerFocused, setIsComposerFocused] = useState(false); |
| | 792 | const isMobileViewport = useMediaQuery("max-sm"); |
| | 793 | const isComposerCollapsedMobile = isMobileViewport && !isComposerFocused; |
| 777 | | 794 | |
| 778 | // ------------------------------------------------------------------ | 795 | // ------------------------------------------------------------------ |
| 779 | // Refs | 796 | // Refs |
| 780 | // ------------------------------------------------------------------ | 797 | // ------------------------------------------------------------------ |
| 781 | const composerEditorRef = useRef<ComposerPromptEditorHandle>(null); | 798 | const composerEditorRef = useRef<ComposerPromptEditorHandle>(null); |
| 782 | const composerFormRef = useRef<HTMLFormElement>(null); | 799 | const composerFormRef = useRef<HTMLFormElement>(null); |
| | 800 | const composerSurfaceRef = useRef<HTMLDivElement>(null); |
| 783 | const composerFormHeightRef = useRef(0); | 801 | const composerFormHeightRef = useRef(0); |
| 784 | const composerSelectLockRef = useRef(false); | 802 | const composerSelectLockRef = useRef(false); |
| 785 | const composerMenuOpenRef = useRef(false); | 803 | const composerMenuOpenRef = useRef(false); |
| 786 | const composerMenuItemsRef = useRef<ComposerCommandItem[]>([]); | 804 | const composerMenuItemsRef = useRef<ComposerCommandItem[]>([]); |
| 787 | const activeComposerMenuItemRef = useRef<ComposerCommandItem | null>(null); | 805 | const activeComposerMenuItemRef = useRef<ComposerCommandItem | null>(null); |
| | 806 | const composerBlurFrameRef = useRef<number | null>(null); |
| | 807 | const mobileComposerExpandFrameRef = useRef<number | null>(null); |
| | 808 | const mobileComposerExpandReleaseFrameRef = useRef<number | null>(null); |
| | 809 | const mobileComposerExpandInFlightRef = useRef(false); |
| 788 | const dragDepthRef = useRef(0); | 810 | const dragDepthRef = useRef(0); |
| 789 | | 811 | |
| 790 | // ------------------------------------------------------------------ | 812 | // ------------------------------------------------------------------ |
| ... | @@ -929,6 +951,8 @@ export const ChatComposer = memo( | ... | @@ -929,6 +951,8 @@ export const ChatComposer = memo( |
| 929 | isComposerApprovalState || | 951 | isComposerApprovalState || |
| 930 | pendingUserInputs.length > 0 || | 952 | pendingUserInputs.length > 0 || |
| 931 | (showPlanFollowUpPrompt && activeProposedPlan !== null); | 953 | (showPlanFollowUpPrompt && activeProposedPlan !== null); |
| | 954 | const showCollapsedMobilePromptRow = |
| | 955 | isComposerCollapsedMobile && !isComposerApprovalState && pendingUserInputs.length === 0; |
| 932 | | 956 | |
| 933 | const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null; | 957 | const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null; |
| 934 | const showPlanSidebarToggle = Boolean(activePlan || sidebarProposedPlan || planSidebarOpen); | 958 | const showPlanSidebarToggle = Boolean(activePlan || sidebarProposedPlan || planSidebarOpen); |
| ... | @@ -1021,6 +1045,11 @@ export const ChatComposer = memo( | ... | @@ -1021,6 +1045,11 @@ export const ChatComposer = memo( |
| 1021 | : null, | 1045 | : null, |
| 1022 | [activePendingIsResponding, activePendingProgress, activePendingResolvedAnswers], | 1046 | [activePendingIsResponding, activePendingProgress, activePendingResolvedAnswers], |
| 1023 | ); | 1047 | ); |
| | 1048 | const collapsedComposerPrimaryActionDisabled = |
| | 1049 | phase === "running" || isSendBusy || isConnecting || !composerSendState.hasSendableContent; |
| | 1050 | const collapsedComposerPrimaryActionLabel = "Send message"; |
| | 1051 | const showMobilePendingAnswerActions = |
| | 1052 | isMobileViewport && !isComposerCollapsedMobile && pendingPrimaryAction !== null; |
| 1024 | | 1053 | |
| 1025 | // ------------------------------------------------------------------ | 1054 | // ------------------------------------------------------------------ |
| 1026 | // Prompt helpers | 1055 | // Prompt helpers |
| ... | @@ -1550,6 +1579,69 @@ export const ChatComposer = memo( | ... | @@ -1550,6 +1579,69 @@ export const ChatComposer = memo( |
| 1550 | [composerHighlightedItemId, composerMenuItems], | 1579 | [composerHighlightedItemId, composerMenuItems], |
| 1551 | ); | 1580 | ); |
| 1552 | | 1581 | |
| | 1582 | const blurMobileComposerAfterSend = useCallback(() => { |
| | 1583 | if (!isMobileViewport) return; |
| | 1584 | if (composerBlurFrameRef.current !== null) { |
| | 1585 | window.cancelAnimationFrame(composerBlurFrameRef.current); |
| | 1586 | composerBlurFrameRef.current = null; |
| | 1587 | } |
| | 1588 | const activeElement = document.activeElement; |
| | 1589 | if (activeElement instanceof HTMLElement) { |
| | 1590 | activeElement.blur(); |
| | 1591 | } |
| | 1592 | setIsComposerFocused(false); |
| | 1593 | }, [isMobileViewport]); |
| | 1594 | |
| | 1595 | const shouldBlurMobileComposerOnSubmit = useCallback(() => { |
| | 1596 | if (!isMobileViewport) return false; |
| | 1597 | if (isSendBusy || isConnecting || phase === "running") return false; |
| | 1598 | if (activePendingProgress) { |
| | 1599 | return activePendingProgress.isLastQuestion && Boolean(activePendingResolvedAnswers); |
| | 1600 | } |
| | 1601 | return showPlanFollowUpPrompt || composerSendState.hasSendableContent; |
| | 1602 | }, [ |
| | 1603 | activePendingProgress, |
| | 1604 | activePendingResolvedAnswers, |
| | 1605 | composerSendState.hasSendableContent, |
| | 1606 | isConnecting, |
| | 1607 | isMobileViewport, |
| | 1608 | isSendBusy, |
| | 1609 | phase, |
| | 1610 | showPlanFollowUpPrompt, |
| | 1611 | ]); |
| | 1612 | |
| | 1613 | const submitComposer = useCallback( |
| | 1614 | (event?: { preventDefault: () => void }) => { |
| | 1615 | onSend(event); |
| | 1616 | if (shouldBlurMobileComposerOnSubmit()) { |
| | 1617 | blurMobileComposerAfterSend(); |
| | 1618 | } |
| | 1619 | }, |
| | 1620 | [blurMobileComposerAfterSend, onSend, shouldBlurMobileComposerOnSubmit], |
| | 1621 | ); |
| | 1622 | const expandMobileComposer = useCallback(() => { |
| | 1623 | if (composerBlurFrameRef.current !== null) { |
| | 1624 | window.cancelAnimationFrame(composerBlurFrameRef.current); |
| | 1625 | composerBlurFrameRef.current = null; |
| | 1626 | } |
| | 1627 | if (mobileComposerExpandFrameRef.current !== null) { |
| | 1628 | window.cancelAnimationFrame(mobileComposerExpandFrameRef.current); |
| | 1629 | } |
| | 1630 | if (mobileComposerExpandReleaseFrameRef.current !== null) { |
| | 1631 | window.cancelAnimationFrame(mobileComposerExpandReleaseFrameRef.current); |
| | 1632 | } |
| | 1633 | mobileComposerExpandInFlightRef.current = true; |
| | 1634 | setIsComposerFocused(true); |
| | 1635 | mobileComposerExpandFrameRef.current = window.requestAnimationFrame(() => { |
| | 1636 | mobileComposerExpandFrameRef.current = null; |
| | 1637 | composerEditorRef.current?.focusAtEnd(); |
| | 1638 | mobileComposerExpandReleaseFrameRef.current = window.requestAnimationFrame(() => { |
| | 1639 | mobileComposerExpandReleaseFrameRef.current = null; |
| | 1640 | mobileComposerExpandInFlightRef.current = false; |
| | 1641 | }); |
| | 1642 | }); |
| | 1643 | }, []); |
| | 1644 | |
| 1553 | // ------------------------------------------------------------------ | 1645 | // ------------------------------------------------------------------ |
| 1554 | // Callbacks: command key | 1646 | // Callbacks: command key |
| 1555 | // ------------------------------------------------------------------ | 1647 | // ------------------------------------------------------------------ |
| ... | @@ -1580,7 +1672,7 @@ export const ChatComposer = memo( | ... | @@ -1580,7 +1672,7 @@ export const ChatComposer = memo( |
| 1580 | } | 1672 | } |
| 1581 | } | 1673 | } |
| 1582 | if (key === "Enter" && !event.shiftKey) { | 1674 | if (key === "Enter" && !event.shiftKey) { |
| 1583 | void onSend(); | 1675 | submitComposer(); |
| 1584 | return true; | 1676 | return true; |
| 1585 | } | 1677 | } |
| 1586 | return false; | 1678 | return false; |
| ... | @@ -1690,6 +1782,50 @@ export const ChatComposer = memo( | ... | @@ -1690,6 +1782,50 @@ export const ChatComposer = memo( |
| 1690 | const handleImplementPlanInNewThreadPrimaryAction = useCallback(() => { | 1782 | const handleImplementPlanInNewThreadPrimaryAction = useCallback(() => { |
| 1691 | void onImplementPlanInNewThread(); | 1783 | void onImplementPlanInNewThread(); |
| 1692 | }, [onImplementPlanInNewThread]); | 1784 | }, [onImplementPlanInNewThread]); |
| | 1785 | const scheduleComposerCollapseCheck = useCallback(() => { |
| | 1786 | if (!isMobileViewport) { |
| | 1787 | return; |
| | 1788 | } |
| | 1789 | if (mobileComposerExpandInFlightRef.current) { |
| | 1790 | return; |
| | 1791 | } |
| | 1792 | if (composerBlurFrameRef.current !== null) { |
| | 1793 | window.cancelAnimationFrame(composerBlurFrameRef.current); |
| | 1794 | } |
| | 1795 | composerBlurFrameRef.current = window.requestAnimationFrame(() => { |
| | 1796 | composerBlurFrameRef.current = null; |
| | 1797 | if (mobileComposerExpandInFlightRef.current) { |
| | 1798 | return; |
| | 1799 | } |
| | 1800 | const composerSurface = composerSurfaceRef.current; |
| | 1801 | const activeElement = document.activeElement; |
| | 1802 | if (activeElement instanceof Element && isInsideComposerFloatingLayer(activeElement)) { |
| | 1803 | return; |
| | 1804 | } |
| | 1805 | if ( |
| | 1806 | composerSurface && |
| | 1807 | activeElement instanceof Node && |
| | 1808 | composerSurface.contains(activeElement) |
| | 1809 | ) { |
| | 1810 | return; |
| | 1811 | } |
| | 1812 | setIsComposerFocused(false); |
| | 1813 | }); |
| | 1814 | }, [isMobileViewport]); |
| | 1815 | |
| | 1816 | useEffect(() => { |
| | 1817 | return () => { |
| | 1818 | if (composerBlurFrameRef.current !== null) { |
| | 1819 | window.cancelAnimationFrame(composerBlurFrameRef.current); |
| | 1820 | } |
| | 1821 | if (mobileComposerExpandFrameRef.current !== null) { |
| | 1822 | window.cancelAnimationFrame(mobileComposerExpandFrameRef.current); |
| | 1823 | } |
| | 1824 | if (mobileComposerExpandReleaseFrameRef.current !== null) { |
| | 1825 | window.cancelAnimationFrame(mobileComposerExpandReleaseFrameRef.current); |
| | 1826 | } |
| | 1827 | }; |
| | 1828 | }, []); |
| 1693 | | 1829 | |
| 1694 | // ------------------------------------------------------------------ | 1830 | // ------------------------------------------------------------------ |
| 1695 | // Imperative handle | 1831 | // Imperative handle |
| ... | @@ -1803,7 +1939,7 @@ export const ChatComposer = memo( | ... | @@ -1803,7 +1939,7 @@ export const ChatComposer = memo( |
| 1803 | return ( | 1939 | return ( |
| 1804 | <form | 1940 | <form |
| 1805 | ref={composerFormRef} | 1941 | ref={composerFormRef} |
| 1806 | onSubmit={onSend} | 1942 | onSubmit={submitComposer} |
| 1807 | className="mx-auto w-full min-w-0 max-w-208" | 1943 | className="mx-auto w-full min-w-0 max-w-208" |
| 1808 | data-chat-composer-form="true" | 1944 | data-chat-composer-form="true" |
| 1809 | > | 1945 | > |
| ... | @@ -1818,21 +1954,82 @@ export const ChatComposer = memo( | ... | @@ -1818,21 +1954,82 @@ export const ChatComposer = memo( |
| 1818 | onDrop={onComposerDrop} | 1954 | onDrop={onComposerDrop} |
| 1819 | > | 1955 | > |
| 1820 | <div | 1956 | <div |
| | 1957 | ref={composerSurfaceRef} |
| | 1958 | data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"} |
| 1821 | className={cn( | 1959 | className={cn( |
| 1822 | "rounded-[20px] border bg-card transition-colors duration-200 has-focus-visible:border-ring/45", | 1960 | "rounded-[20px] border bg-card transition-colors duration-200 has-focus-visible:border-ring/45", |
| 1823 | isDragOverComposer ? "border-primary/70 bg-accent/30" : "border-border", | 1961 | isDragOverComposer ? "border-primary/70 bg-accent/30" : "border-border", |
| 1824 | composerProviderState.composerSurfaceClassName, | 1962 | composerProviderState.composerSurfaceClassName, |
| 1825 | )} | 1963 | )} |
| | 1964 | onFocusCapture={(event) => { |
| | 1965 | const activeElement = event.target; |
| | 1966 | if ( |
| | 1967 | isComposerCollapsedMobile && |
| | 1968 | activeElement instanceof HTMLElement && |
| | 1969 | activeElement.closest('[data-chat-composer-collapsed-controls="true"]') |
| | 1970 | ) { |
| | 1971 | return; |
| | 1972 | } |
| | 1973 | if (composerBlurFrameRef.current !== null) { |
| | 1974 | window.cancelAnimationFrame(composerBlurFrameRef.current); |
| | 1975 | composerBlurFrameRef.current = null; |
| | 1976 | } |
| | 1977 | setIsComposerFocused(true); |
| | 1978 | }} |
| | 1979 | onBlurCapture={() => { |
| | 1980 | scheduleComposerCollapseCheck(); |
| | 1981 | }} |
| 1826 | > | 1982 | > |
| 1827 | {activePendingApproval ? ( | 1983 | {!isComposerCollapsedMobile && |
| 1828 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> | 1984 | (activePendingApproval ? ( |
| | 1985 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| | 1986 | <ComposerPendingApprovalPanel |
| | 1987 | approval={activePendingApproval} |
| | 1988 | pendingCount={pendingApprovals.length} |
| | 1989 | /> |
| | 1990 | </div> |
| | 1991 | ) : pendingUserInputs.length > 0 ? ( |
| | 1992 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| | 1993 | <ComposerPendingUserInputPanel |
| | 1994 | pendingUserInputs={pendingUserInputs} |
| | 1995 | respondingRequestIds={respondingRequestIds} |
| | 1996 | answers={activePendingDraftAnswers} |
| | 1997 | questionIndex={activePendingQuestionIndex} |
| | 1998 | onToggleOption={onSelectActivePendingUserInputOption} |
| | 1999 | onAdvance={onAdvanceActivePendingUserInput} |
| | 2000 | /> |
| | 2001 | </div> |
| | 2002 | ) : showPlanFollowUpPrompt && activeProposedPlan ? ( |
| | 2003 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| | 2004 | <ComposerPlanFollowUpBanner |
| | 2005 | key={activeProposedPlan.id} |
| | 2006 | planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null} |
| | 2007 | /> |
| | 2008 | </div> |
| | 2009 | ) : null)} |
| | 2010 | |
| | 2011 | {isComposerCollapsedMobile && activePendingApproval ? ( |
| | 2012 | <div |
| | 2013 | className="rounded-t-[19px] border-b border-border/65 bg-muted/20" |
| | 2014 | data-chat-composer-collapsed-controls="true" |
| | 2015 | > |
| 1829 | <ComposerPendingApprovalPanel | 2016 | <ComposerPendingApprovalPanel |
| 1830 | approval={activePendingApproval} | 2017 | approval={activePendingApproval} |
| 1831 | pendingCount={pendingApprovals.length} | 2018 | pendingCount={pendingApprovals.length} |
| 1832 | /> | 2019 | /> |
| | 2020 | <div className="flex flex-wrap items-center justify-end gap-2 px-3 pb-3 sm:px-4"> |
| | 2021 | <ComposerPendingApprovalActions |
| | 2022 | requestId={activePendingApproval.requestId} |
| | 2023 | isResponding={respondingRequestIds.includes(activePendingApproval.requestId)} |
| | 2024 | onRespondToApproval={onRespondToApproval} |
| | 2025 | /> |
| | 2026 | </div> |
| 1833 | </div> | 2027 | </div> |
| 1834 | ) : pendingUserInputs.length > 0 ? ( | 2028 | ) : isComposerCollapsedMobile && pendingUserInputs.length > 0 ? ( |
| 1835 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> | 2029 | <div |
| | 2030 | className="rounded-t-[19px] border-b border-border/65 bg-muted/20" |
| | 2031 | data-chat-composer-collapsed-controls="true" |
| | 2032 | > |
| 1836 | <ComposerPendingUserInputPanel | 2033 | <ComposerPendingUserInputPanel |
| 1837 | pendingUserInputs={pendingUserInputs} | 2034 | pendingUserInputs={pendingUserInputs} |
| 1838 | respondingRequestIds={respondingRequestIds} | 2035 | respondingRequestIds={respondingRequestIds} |
| ... | @@ -1841,13 +2038,91 @@ export const ChatComposer = memo( | ... | @@ -1841,13 +2038,91 @@ export const ChatComposer = memo( |
| 1841 | onToggleOption={onSelectActivePendingUserInputOption} | 2038 | onToggleOption={onSelectActivePendingUserInputOption} |
| 1842 | onAdvance={onAdvanceActivePendingUserInput} | 2039 | onAdvance={onAdvanceActivePendingUserInput} |
| 1843 | /> | 2040 | /> |
| | 2041 | <div className="px-3 pb-3 sm:px-4"> |
| | 2042 | <div |
| | 2043 | data-chat-composer-mobile-pending-compact="true" |
| | 2044 | className={cn( |
| | 2045 | "flex min-w-0 items-center gap-2 rounded-lg border border-border/55 bg-background/55 p-1.5 pl-3 transition-colors hover:bg-background/80", |
| | 2046 | !activePendingProgress?.activeQuestion?.multiSelect && "p-0", |
| | 2047 | )} |
| | 2048 | > |
| | 2049 | <button |
| | 2050 | type="button" |
| | 2051 | className={cn( |
| | 2052 | "min-w-0 flex-1 truncate bg-transparent py-1.5 text-left text-sm", |
| | 2053 | activePendingProgress?.customAnswer |
| | 2054 | ? "text-foreground" |
| | 2055 | : "text-muted-foreground/60", |
| | 2056 | !activePendingProgress?.activeQuestion?.multiSelect && "px-3 py-2", |
| | 2057 | )} |
| | 2058 | onPointerDown={(event) => event.preventDefault()} |
| | 2059 | onClick={expandMobileComposer} |
| | 2060 | aria-label="Write custom answer" |
| | 2061 | > |
| | 2062 | {activePendingProgress?.customAnswer || "Write custom answer"} |
| | 2063 | </button> |
| | 2064 | {activePendingProgress?.activeQuestion?.multiSelect ? ( |
| | 2065 | <ComposerPrimaryActions |
| | 2066 | compact |
| | 2067 | pendingAction={pendingPrimaryAction} |
| | 2068 | isRunning={false} |
| | 2069 | showPlanFollowUpPrompt={false} |
| | 2070 | promptHasText={false} |
| | 2071 | isSendBusy={isSendBusy} |
| | 2072 | isConnecting={isConnecting} |
| | 2073 | isPreparingWorktree={false} |
| | 2074 | hasSendableContent={false} |
| | 2075 | preserveComposerFocusOnPointerDown |
| | 2076 | onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} |
| | 2077 | onInterrupt={handleInterruptPrimaryAction} |
| | 2078 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} |
| | 2079 | /> |
| | 2080 | ) : null} |
| | 2081 | </div> |
| | 2082 | </div> |
| 1844 | </div> | 2083 | </div> |
| 1845 | ) : showPlanFollowUpPrompt && activeProposedPlan ? ( | 2084 | ) : null} |
| 1846 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> | 2085 | |
| 1847 | <ComposerPlanFollowUpBanner | 2086 | {showCollapsedMobilePromptRow ? ( |
| 1848 | key={activeProposedPlan.id} | 2087 | <div className="flex items-center justify-between gap-2 px-3 py-2"> |
| 1849 | planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null} | 2088 | <button |
| 1850 | /> | 2089 | type="button" |
| | 2090 | className={cn( |
| | 2091 | "min-w-0 flex-1 truncate bg-transparent p-0 text-left text-[14px] focus:outline-none", |
| | 2092 | (activePendingProgress ? activePendingProgress.customAnswer : prompt.trim()) |
| | 2093 | ? "text-foreground" |
| | 2094 | : "text-muted-foreground/35", |
| | 2095 | )} |
| | 2096 | onPointerDown={(event) => event.preventDefault()} |
| | 2097 | onClick={expandMobileComposer} |
| | 2098 | aria-label="Expand composer" |
| | 2099 | > |
| | 2100 | {activePendingProgress |
| | 2101 | ? activePendingProgress.customAnswer || |
| | 2102 | "Type your own answer, or leave this blank to use the selected option" |
| | 2103 | : prompt.trim() || "Ask anything..."} |
| | 2104 | </button> |
| | 2105 | <button |
| | 2106 | type="button" |
| | 2107 | className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/90 text-primary-foreground disabled:opacity-30" |
| | 2108 | disabled={collapsedComposerPrimaryActionDisabled} |
| | 2109 | aria-label={collapsedComposerPrimaryActionLabel} |
| | 2110 | onPointerDown={(event) => event.preventDefault()} |
| | 2111 | onClick={(event) => { |
| | 2112 | event.stopPropagation(); |
| | 2113 | submitComposer(); |
| | 2114 | }} |
| | 2115 | > |
| | 2116 | <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> |
| | 2117 | <path |
| | 2118 | d="M8 3L8 13M8 3L4 7M8 3L12 7" |
| | 2119 | stroke="currentColor" |
| | 2120 | strokeWidth="2" |
| | 2121 | strokeLinecap="round" |
| | 2122 | strokeLinejoin="round" |
| | 2123 | /> |
| | 2124 | </svg> |
| | 2125 | </button> |
| 1851 | </div> | 2126 | </div> |
| 1852 | ) : null} | 2127 | ) : null} |
| 1853 | | 2128 | |
| ... | @@ -1855,6 +2130,7 @@ export const ChatComposer = memo( | ... | @@ -1855,6 +2130,7 @@ export const ChatComposer = memo( |
| 1855 | className={cn( | 2130 | className={cn( |
| 1856 | "relative px-3 pb-2 sm:px-4", | 2131 | "relative px-3 pb-2 sm:px-4", |
| 1857 | hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4", | 2132 | hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4", |
| | 2133 | isComposerCollapsedMobile && "hidden", |
| 1858 | )} | 2134 | )} |
| 1859 | > | 2135 | > |
| 1860 | {composerMenuOpen && !isComposerApprovalState && ( | 2136 | {composerMenuOpen && !isComposerApprovalState && ( |
| ... | @@ -1876,7 +2152,8 @@ export const ChatComposer = memo( | ... | @@ -1876,7 +2152,8 @@ export const ChatComposer = memo( |
| 1876 | </div> | 2152 | </div> |
| 1877 | )} | 2153 | )} |
| 1878 | | 2154 | |
| 1879 | {!isComposerApprovalState && | 2155 | {!isComposerCollapsedMobile && |
| | 2156 | !isComposerApprovalState && |
| 1880 | pendingUserInputs.length === 0 && | 2157 | pendingUserInputs.length === 0 && |
| 1881 | composerImages.length > 0 && ( | 2158 | composerImages.length > 0 && ( |
| 1882 | <div className="mb-3 flex flex-wrap gap-2"> | 2159 | <div className="mb-3 flex flex-wrap gap-2"> |
| ... | @@ -1943,43 +2220,69 @@ export const ChatComposer = memo( | ... | @@ -1943,43 +2220,69 @@ export const ChatComposer = memo( |
| 1943 | </div> | 2220 | </div> |
| 1944 | )} | 2221 | )} |
| 1945 | | 2222 | |
| 1946 | <ComposerPromptEditor | 2223 | <div className="relative"> |
| 1947 | ref={composerEditorRef} | 2224 | <ComposerPromptEditor |
| 1948 | value={ | 2225 | ref={composerEditorRef} |
| 1949 | isComposerApprovalState | 2226 | value={ |
| 1950 | ? "" | 2227 | isComposerApprovalState |
| 1951 | : activePendingProgress | 2228 | ? "" |
| 1952 | ? activePendingProgress.customAnswer | 2229 | : activePendingProgress |
| 1953 | : prompt | 2230 | ? activePendingProgress.customAnswer |
| 1954 | } | 2231 | : prompt |
| 1955 | cursor={composerCursor} | 2232 | } |
| 1956 | terminalContexts={ | 2233 | cursor={composerCursor} |
| 1957 | !isComposerApprovalState && pendingUserInputs.length === 0 | 2234 | terminalContexts={ |
| 1958 | ? composerTerminalContexts | 2235 | !isComposerApprovalState && pendingUserInputs.length === 0 |
| 1959 | : [] | 2236 | ? composerTerminalContexts |
| 1960 | } | 2237 | : [] |
| 1961 | skills={selectedProviderStatus?.skills ?? []} | 2238 | } |
| 1962 | onRemoveTerminalContext={removeComposerTerminalContextFromDraft} | 2239 | skills={selectedProviderStatus?.skills ?? []} |
| 1963 | onChange={onPromptChange} | 2240 | {...(showMobilePendingAnswerActions ? { className: "max-sm:pb-11" } : {})} |
| 1964 | onCommandKeyDown={onComposerCommandKey} | 2241 | onRemoveTerminalContext={removeComposerTerminalContextFromDraft} |
| 1965 | onPaste={onComposerPaste} | 2242 | onChange={onPromptChange} |
| 1966 | placeholder={ | 2243 | onCommandKeyDown={onComposerCommandKey} |
| 1967 | isComposerApprovalState | 2244 | onPaste={onComposerPaste} |
| 1968 | ? (activePendingApproval?.detail ?? "Resolve this approval request to continue") | 2245 | placeholder={ |
| 1969 | : activePendingProgress | 2246 | isComposerApprovalState |
| 1970 | ? "Type your own answer, or leave this blank to use the selected option" | 2247 | ? (activePendingApproval?.detail ?? |
| 1971 | : showPlanFollowUpPrompt && activeProposedPlan | 2248 | "Resolve this approval request to continue") |
| 1972 | ? "Add feedback to refine the plan, or leave this blank to implement it" | 2249 | : activePendingProgress |
| 1973 | : phase === "disconnected" | 2250 | ? "Type your own answer, or leave this blank to use the selected option" |
| 1974 | ? "Ask for follow-up changes or attach images" | 2251 | : showPlanFollowUpPrompt && activeProposedPlan |
| 1975 | : "Ask anything, @tag files/folders, or use / to show available commands" | 2252 | ? "Add feedback to refine the plan, or leave this blank to implement it" |
| 1976 | } | 2253 | : phase === "disconnected" |
| 1977 | disabled={isConnecting || isComposerApprovalState} | 2254 | ? "Ask for follow-up changes or attach images" |
| 1978 | /> | 2255 | : "Ask anything, @tag files/folders, or use / to show available commands" |
| | 2256 | } |
| | 2257 | disabled={isConnecting || isComposerApprovalState} |
| | 2258 | /> |
| | 2259 | {showMobilePendingAnswerActions ? ( |
| | 2260 | <div |
| | 2261 | data-chat-composer-mobile-pending-actions="true" |
| | 2262 | className="absolute bottom-0 right-0 flex justify-end" |
| | 2263 | > |
| | 2264 | <ComposerPrimaryActions |
| | 2265 | compact |
| | 2266 | pendingAction={pendingPrimaryAction} |
| | 2267 | isRunning={false} |
| | 2268 | showPlanFollowUpPrompt={false} |
| | 2269 | promptHasText={false} |
| | 2270 | isSendBusy={isSendBusy} |
| | 2271 | isConnecting={isConnecting} |
| | 2272 | isPreparingWorktree={false} |
| | 2273 | hasSendableContent={false} |
| | 2274 | preserveComposerFocusOnPointerDown |
| | 2275 | onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} |
| | 2276 | onInterrupt={handleInterruptPrimaryAction} |
| | 2277 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} |
| | 2278 | /> |
| | 2279 | </div> |
| | 2280 | ) : null} |
| | 2281 | </div> |
| 1979 | </div> | 2282 | </div> |
| 1980 | | 2283 | |
| 1981 | {/* Bottom toolbar */} | 2284 | {/* Bottom toolbar */} |
| 1982 | {activePendingApproval ? ( | 2285 | {isComposerCollapsedMobile ? null : activePendingApproval ? ( |
| 1983 | <div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3"> | 2286 | <div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3"> |
| 1984 | <ComposerPendingApprovalActions | 2287 | <ComposerPendingApprovalActions |
| 1985 | requestId={activePendingApproval.requestId} | 2288 | requestId={activePendingApproval.requestId} |
| ... | @@ -1994,6 +2297,7 @@ export const ChatComposer = memo( | ... | @@ -1994,6 +2297,7 @@ export const ChatComposer = memo( |
| 1994 | className={cn( | 2297 | className={cn( |
| 1995 | "flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-2.5 pb-2.5 sm:px-3 sm:pb-3", | 2298 | "flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-2.5 pb-2.5 sm:px-3 sm:pb-3", |
| 1996 | isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0", | 2299 | isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0", |
| | 2300 | showMobilePendingAnswerActions && "hidden sm:flex", |
| 1997 | )} | 2301 | )} |
| 1998 | > | 2302 | > |
| 1999 | <div className="-m-1 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> | 2303 | <div className="-m-1 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> |
| ... | @@ -2082,6 +2386,7 @@ export const ChatComposer = memo( | ... | @@ -2082,6 +2386,7 @@ export const ChatComposer = memo( |
| 2082 | isConnecting={isConnecting} | 2386 | isConnecting={isConnecting} |
| 2083 | isPreparingWorktree={isPreparingWorktree} | 2387 | isPreparingWorktree={isPreparingWorktree} |
| 2084 | hasSendableContent={composerSendState.hasSendableContent} | 2388 | hasSendableContent={composerSendState.hasSendableContent} |
| | 2389 | preserveComposerFocusOnPointerDown={isMobileViewport} |
| 2085 | onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} | 2390 | onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} |
| 2086 | onInterrupt={handleInterruptPrimaryAction} | 2391 | onInterrupt={handleInterruptPrimaryAction} |
| 2087 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} | 2392 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} |