| ... | ... | @@ -111,6 +111,7 @@ import type { PendingApproval, PendingUserInput } from "../../session-logic"; |
| 111 | 111 | import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow"; |
| 112 | 112 | import { formatProviderSkillDisplayName } from "../../providerSkillPresentation"; |
| 113 | 113 | import { searchProviderSkills } from "../../providerSkillSearch"; |
| 114 | import { useMediaQuery } from "../../hooks/useMediaQuery"; |
| 114 | 115 | |
| 115 | 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 | 139 | const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; |
| 139 | 140 | const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; |
| 140 | 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 | 150 | const extendReplacementRangeForTrailingSpace = ( |
| 143 | 151 | text: string, |
| ... | ... | @@ -167,6 +175,10 @@ const terminalContextIdListsEqual = ( |
| 167 | 175 | ): boolean => |
| 168 | 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 | 182 | const ComposerFooterModeControls = memo(function ComposerFooterModeControls(props: { |
| 171 | 183 | showInteractionModeToggle: boolean; |
| 172 | 184 | interactionMode: ProviderInteractionMode; |
| ... | ... | @@ -290,6 +302,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 290 | 302 | isSendBusy: boolean; |
| 291 | 303 | isConnecting: boolean; |
| 292 | 304 | hasSendableContent: boolean; |
| 305 | preserveComposerFocusOnPointerDown?: boolean; |
| 293 | 306 | onPreviousPendingQuestion: () => void; |
| 294 | 307 | onInterrupt: () => void; |
| 295 | 308 | onImplementPlanInNewThread: () => void; |
| ... | ... | @@ -310,6 +323,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 310 | 323 | isConnecting={props.isConnecting} |
| 311 | 324 | isPreparingWorktree={props.isPreparingWorktree} |
| 312 | 325 | hasSendableContent={props.hasSendableContent} |
| 326 | preserveComposerFocusOnPointerDown={props.preserveComposerFocusOnPointerDown ?? false} |
| 313 | 327 | onPreviousPendingQuestion={props.onPreviousPendingQuestion} |
| 314 | 328 | onInterrupt={props.onInterrupt} |
| 315 | 329 | onImplementPlanInNewThread={props.onImplementPlanInNewThread} |
| ... | ... | @@ -389,7 +403,7 @@ export interface ChatComposerProps { |
| 389 | 403 | isLastQuestion: boolean; |
| 390 | 404 | canAdvance: boolean; |
| 391 | 405 | customAnswer: string; |
| 392 | | activeQuestion: { id: string } | null; |
| 406 | activeQuestion: { id: string; multiSelect?: boolean | undefined } | null; |
| 393 | 407 | } | null; |
| 394 | 408 | activePendingResolvedAnswers: Record<string, unknown> | null; |
| 395 | 409 | activePendingIsResponding: boolean; |
| ... | ... | @@ -774,17 +788,25 @@ export const ChatComposer = memo( |
| 774 | 788 | const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); |
| 775 | 789 | const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false); |
| 776 | 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 | 796 | // Refs |
| 780 | 797 | // ------------------------------------------------------------------ |
| 781 | 798 | const composerEditorRef = useRef<ComposerPromptEditorHandle>(null); |
| 782 | 799 | const composerFormRef = useRef<HTMLFormElement>(null); |
| 800 | const composerSurfaceRef = useRef<HTMLDivElement>(null); |
| 783 | 801 | const composerFormHeightRef = useRef(0); |
| 784 | 802 | const composerSelectLockRef = useRef(false); |
| 785 | 803 | const composerMenuOpenRef = useRef(false); |
| 786 | 804 | const composerMenuItemsRef = useRef<ComposerCommandItem[]>([]); |
| 787 | 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 | 810 | const dragDepthRef = useRef(0); |
| 789 | 811 | |
| 790 | 812 | // ------------------------------------------------------------------ |
| ... | ... | @@ -929,6 +951,8 @@ export const ChatComposer = memo( |
| 929 | 951 | isComposerApprovalState || |
| 930 | 952 | pendingUserInputs.length > 0 || |
| 931 | 953 | (showPlanFollowUpPrompt && activeProposedPlan !== null); |
| 954 | const showCollapsedMobilePromptRow = |
| 955 | isComposerCollapsedMobile && !isComposerApprovalState && pendingUserInputs.length === 0; |
| 932 | 956 | |
| 933 | 957 | const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null; |
| 934 | 958 | const showPlanSidebarToggle = Boolean(activePlan || sidebarProposedPlan || planSidebarOpen); |
| ... | ... | @@ -1021,6 +1045,11 @@ export const ChatComposer = memo( |
| 1021 | 1045 | : null, |
| 1022 | 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 | 1055 | // Prompt helpers |
| ... | ... | @@ -1550,6 +1579,69 @@ export const ChatComposer = memo( |
| 1550 | 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 | 1646 | // Callbacks: command key |
| 1555 | 1647 | // ------------------------------------------------------------------ |
| ... | ... | @@ -1580,7 +1672,7 @@ export const ChatComposer = memo( |
| 1580 | 1672 | } |
| 1581 | 1673 | } |
| 1582 | 1674 | if (key === "Enter" && !event.shiftKey) { |
| 1583 | | void onSend(); |
| 1675 | submitComposer(); |
| 1584 | 1676 | return true; |
| 1585 | 1677 | } |
| 1586 | 1678 | return false; |
| ... | ... | @@ -1690,6 +1782,50 @@ export const ChatComposer = memo( |
| 1690 | 1782 | const handleImplementPlanInNewThreadPrimaryAction = useCallback(() => { |
| 1691 | 1783 | void onImplementPlanInNewThread(); |
| 1692 | 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 | 1831 | // Imperative handle |
| ... | ... | @@ -1803,7 +1939,7 @@ export const ChatComposer = memo( |
| 1803 | 1939 | return ( |
| 1804 | 1940 | <form |
| 1805 | 1941 | ref={composerFormRef} |
| 1806 | | onSubmit={onSend} |
| 1942 | onSubmit={submitComposer} |
| 1807 | 1943 | className="mx-auto w-full min-w-0 max-w-208" |
| 1808 | 1944 | data-chat-composer-form="true" |
| 1809 | 1945 | > |
| ... | ... | @@ -1818,21 +1954,82 @@ export const ChatComposer = memo( |
| 1818 | 1954 | onDrop={onComposerDrop} |
| 1819 | 1955 | > |
| 1820 | 1956 | <div |
| 1957 | ref={composerSurfaceRef} |
| 1958 | data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"} |
| 1821 | 1959 | className={cn( |
| 1822 | 1960 | "rounded-[20px] border bg-card transition-colors duration-200 has-focus-visible:border-ring/45", |
| 1823 | 1961 | isDragOverComposer ? "border-primary/70 bg-accent/30" : "border-border", |
| 1824 | 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 ? ( |
| 1828 | | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| 1983 | {!isComposerCollapsedMobile && |
| 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 | 2016 | <ComposerPendingApprovalPanel |
| 1830 | 2017 | approval={activePendingApproval} |
| 1831 | 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 | 2027 | </div> |
| 1834 | | ) : pendingUserInputs.length > 0 ? ( |
| 1835 | | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| 2028 | ) : isComposerCollapsedMobile && pendingUserInputs.length > 0 ? ( |
| 2029 | <div |
| 2030 | className="rounded-t-[19px] border-b border-border/65 bg-muted/20" |
| 2031 | data-chat-composer-collapsed-controls="true" |
| 2032 | > |
| 1836 | 2033 | <ComposerPendingUserInputPanel |
| 1837 | 2034 | pendingUserInputs={pendingUserInputs} |
| 1838 | 2035 | respondingRequestIds={respondingRequestIds} |
| ... | ... | @@ -1841,13 +2038,91 @@ export const ChatComposer = memo( |
| 1841 | 2038 | onToggleOption={onSelectActivePendingUserInputOption} |
| 1842 | 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 | 2083 | </div> |
| 1845 | | ) : showPlanFollowUpPrompt && activeProposedPlan ? ( |
| 1846 | | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| 1847 | | <ComposerPlanFollowUpBanner |
| 1848 | | key={activeProposedPlan.id} |
| 1849 | | planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null} |
| 1850 | | /> |
| 2084 | ) : null} |
| 2085 | |
| 2086 | {showCollapsedMobilePromptRow ? ( |
| 2087 | <div className="flex items-center justify-between gap-2 px-3 py-2"> |
| 2088 | <button |
| 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 | 2126 | </div> |
| 1852 | 2127 | ) : null} |
| 1853 | 2128 | |
| ... | ... | @@ -1855,6 +2130,7 @@ export const ChatComposer = memo( |
| 1855 | 2130 | className={cn( |
| 1856 | 2131 | "relative px-3 pb-2 sm:px-4", |
| 1857 | 2132 | hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4", |
| 2133 | isComposerCollapsedMobile && "hidden", |
| 1858 | 2134 | )} |
| 1859 | 2135 | > |
| 1860 | 2136 | {composerMenuOpen && !isComposerApprovalState && ( |
| ... | ... | @@ -1876,7 +2152,8 @@ export const ChatComposer = memo( |
| 1876 | 2152 | </div> |
| 1877 | 2153 | )} |
| 1878 | 2154 | |
| 1879 | | {!isComposerApprovalState && |
| 2155 | {!isComposerCollapsedMobile && |
| 2156 | !isComposerApprovalState && |
| 1880 | 2157 | pendingUserInputs.length === 0 && |
| 1881 | 2158 | composerImages.length > 0 && ( |
| 1882 | 2159 | <div className="mb-3 flex flex-wrap gap-2"> |
| ... | ... | @@ -1943,43 +2220,69 @@ export const ChatComposer = memo( |
| 1943 | 2220 | </div> |
| 1944 | 2221 | )} |
| 1945 | 2222 | |
| 1946 | | <ComposerPromptEditor |
| 1947 | | ref={composerEditorRef} |
| 1948 | | value={ |
| 1949 | | isComposerApprovalState |
| 1950 | | ? "" |
| 1951 | | : activePendingProgress |
| 1952 | | ? activePendingProgress.customAnswer |
| 1953 | | : prompt |
| 1954 | | } |
| 1955 | | cursor={composerCursor} |
| 1956 | | terminalContexts={ |
| 1957 | | !isComposerApprovalState && pendingUserInputs.length === 0 |
| 1958 | | ? composerTerminalContexts |
| 1959 | | : [] |
| 1960 | | } |
| 1961 | | skills={selectedProviderStatus?.skills ?? []} |
| 1962 | | onRemoveTerminalContext={removeComposerTerminalContextFromDraft} |
| 1963 | | onChange={onPromptChange} |
| 1964 | | onCommandKeyDown={onComposerCommandKey} |
| 1965 | | onPaste={onComposerPaste} |
| 1966 | | placeholder={ |
| 1967 | | isComposerApprovalState |
| 1968 | | ? (activePendingApproval?.detail ?? "Resolve this approval request to continue") |
| 1969 | | : activePendingProgress |
| 1970 | | ? "Type your own answer, or leave this blank to use the selected option" |
| 1971 | | : showPlanFollowUpPrompt && activeProposedPlan |
| 1972 | | ? "Add feedback to refine the plan, or leave this blank to implement it" |
| 1973 | | : phase === "disconnected" |
| 1974 | | ? "Ask for follow-up changes or attach images" |
| 1975 | | : "Ask anything, @tag files/folders, or use / to show available commands" |
| 1976 | | } |
| 1977 | | disabled={isConnecting || isComposerApprovalState} |
| 1978 | | /> |
| 2223 | <div className="relative"> |
| 2224 | <ComposerPromptEditor |
| 2225 | ref={composerEditorRef} |
| 2226 | value={ |
| 2227 | isComposerApprovalState |
| 2228 | ? "" |
| 2229 | : activePendingProgress |
| 2230 | ? activePendingProgress.customAnswer |
| 2231 | : prompt |
| 2232 | } |
| 2233 | cursor={composerCursor} |
| 2234 | terminalContexts={ |
| 2235 | !isComposerApprovalState && pendingUserInputs.length === 0 |
| 2236 | ? composerTerminalContexts |
| 2237 | : [] |
| 2238 | } |
| 2239 | skills={selectedProviderStatus?.skills ?? []} |
| 2240 | {...(showMobilePendingAnswerActions ? { className: "max-sm:pb-11" } : {})} |
| 2241 | onRemoveTerminalContext={removeComposerTerminalContextFromDraft} |
| 2242 | onChange={onPromptChange} |
| 2243 | onCommandKeyDown={onComposerCommandKey} |
| 2244 | onPaste={onComposerPaste} |
| 2245 | placeholder={ |
| 2246 | isComposerApprovalState |
| 2247 | ? (activePendingApproval?.detail ?? |
| 2248 | "Resolve this approval request to continue") |
| 2249 | : activePendingProgress |
| 2250 | ? "Type your own answer, or leave this blank to use the selected option" |
| 2251 | : showPlanFollowUpPrompt && activeProposedPlan |
| 2252 | ? "Add feedback to refine the plan, or leave this blank to implement it" |
| 2253 | : phase === "disconnected" |
| 2254 | ? "Ask for follow-up changes or attach images" |
| 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 | 2282 | </div> |
| 1980 | 2283 | |
| 1981 | 2284 | {/* Bottom toolbar */} |
| 1982 | | {activePendingApproval ? ( |
| 2285 | {isComposerCollapsedMobile ? null : activePendingApproval ? ( |
| 1983 | 2286 | <div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3"> |
| 1984 | 2287 | <ComposerPendingApprovalActions |
| 1985 | 2288 | requestId={activePendingApproval.requestId} |
| ... | ... | @@ -1994,6 +2297,7 @@ export const ChatComposer = memo( |
| 1994 | 2297 | className={cn( |
| 1995 | 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 | 2299 | isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0", |
| 2300 | showMobilePendingAnswerActions && "hidden sm:flex", |
| 1997 | 2301 | )} |
| 1998 | 2302 | > |
| 1999 | 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 | 2386 | isConnecting={isConnecting} |
| 2083 | 2387 | isPreparingWorktree={isPreparingWorktree} |
| 2084 | 2388 | hasSendableContent={composerSendState.hasSendableContent} |
| 2389 | preserveComposerFocusOnPointerDown={isMobileViewport} |
| 2085 | 2390 | onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} |
| 2086 | 2391 | onInterrupt={handleInterruptPrimaryAction} |
| 2087 | 2392 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} |