@@ -107,6 +107,12 @@ import { buildExpandedImagePreview, type ExpandedImagePreview } from "./Expanded
107107import { basenameOfPath } from "../../pierre-icons" ;
108108import { cn , randomUUID } from "~/lib/utils" ;
109109import { Separator } from "../ui/separator" ;
110+ import {
111+ getComposerPromptLengthValidationMessage ,
112+ getComposerSubmissionValidationMessage ,
113+ submitComposerDraft ,
114+ } from "./composerSubmission" ;
115+ import { ComposerPromptLengthValidation } from "./ComposerPromptLengthValidation" ;
110116
111117type ComposerCommandMenuPosition = {
112118 bottom : number ;
@@ -488,6 +494,8 @@ export interface ChatComposerHandle {
488494 selectedModel : string ;
489495 selectedProviderModels : ReadonlyArray < ServerProvider [ "models" ] [ number ] > ;
490496 } ;
497+ /** Validate the fully composed text immediately before a provider turn starts. */
498+ validateProviderInput : ( providerInput : string ) => boolean ;
491499}
492500
493501// --------------------------------------------------------------------------
@@ -951,6 +959,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
951959 const [ isComposerPrimaryActionsCompact , setIsComposerPrimaryActionsCompact ] = useState ( false ) ;
952960 const [ isComposerModelPickerOpen , setIsComposerModelPickerOpen ] = useState ( false ) ;
953961 const [ isComposerFocused , setIsComposerFocused ] = useState ( false ) ;
962+ const [ composerSubmissionError , setComposerSubmissionError ] = useState < string | null > ( null ) ;
963+ const [ providerInputSubmissionError , setProviderInputSubmissionError ] = useState < string | null > (
964+ null ,
965+ ) ;
954966 const [ composerMenuAnchor , setComposerMenuAnchor ] = useState < HTMLDivElement | null > ( null ) ;
955967 const [ isStashMenuOpen , setIsStashMenuOpen ] = useState ( false ) ;
956968 const [ stashPulse , setStashPulse ] = useState < { key : number ; active : boolean } > ( {
@@ -967,6 +979,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
967979 const composerEditorRef = useRef < ComposerPromptEditorHandle > ( null ) ;
968980 const composerFormRef = useRef < HTMLFormElement > ( null ) ;
969981 const composerSurfaceRef = useRef < HTMLDivElement > ( null ) ;
982+ const providerInputRejectedRef = useRef ( false ) ;
970983 const composerSelectLockRef = useRef ( false ) ;
971984 const composerMenuOpenRef = useRef ( false ) ;
972985 const composerMenuItemsRef = useRef < ComposerCommandItem [ ] > ( [ ] ) ;
@@ -1309,6 +1322,27 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
13091322 setComposerCursor ( ( existing ) => clampCollapsedComposerCursor ( prompt , existing ) ) ;
13101323 } , [ prompt , promptRef ] ) ;
13111324
1325+ useEffect ( ( ) => {
1326+ if ( composerSubmissionError === null ) return ;
1327+ const nextError = getComposerPromptLengthValidationMessage ( prompt ) ;
1328+ if ( nextError !== composerSubmissionError ) {
1329+ setComposerSubmissionError ( nextError ) ;
1330+ }
1331+ } , [ composerSubmissionError , prompt ] ) ;
1332+
1333+ useEffect ( ( ) => {
1334+ setProviderInputSubmissionError ( null ) ;
1335+ } , [
1336+ composerElementContexts ,
1337+ composerPreviewAnnotations ,
1338+ composerReviewComments ,
1339+ composerTerminalContexts ,
1340+ prompt ,
1341+ selectedModel ,
1342+ selectedPromptEffort ,
1343+ selectedProvider ,
1344+ ] ) ;
1345+
13121346 useEffect ( ( ) => {
13131347 composerImagesRef . current = composerImages ;
13141348 } , [ composerImages , composerImagesRef ] ) ;
@@ -1400,6 +1434,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
14001434 // ------------------------------------------------------------------
14011435 useEffect ( ( ) => {
14021436 setComposerHighlightedItemId ( null ) ;
1437+ setComposerSubmissionError ( null ) ;
1438+ setProviderInputSubmissionError ( null ) ;
14031439 setComposerCursor ( collapseExpandedComposerCursor ( promptRef . current , promptRef . current . length ) ) ;
14041440 setComposerTrigger ( detectComposerTrigger ( promptRef . current , promptRef . current . length ) ) ;
14051441 setIsDragOverComposer ( false ) ;
@@ -1826,17 +1862,32 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
18261862 } ) ;
18271863 return ;
18281864 }
1829- onSend ( event ) ;
1865+ const submission = submitComposerDraft ( {
1866+ prompt : promptRef . current ,
1867+ submissionTarget : activePendingProgress ? "pending-user-input" : "provider-turn" ,
1868+ event,
1869+ onSend : ( sendEvent ) => {
1870+ // ChatView reports its final composed-input preflight through the
1871+ // composer handle before its first asynchronous send step.
1872+ providerInputRejectedRef . current = false ;
1873+ onSend ( sendEvent ) ;
1874+ return ! providerInputRejectedRef . current ;
1875+ } ,
1876+ } ) ;
1877+ setComposerSubmissionError ( submission . validationMessage ) ;
1878+ if ( ! submission . didDispatch ) return ;
18301879 if ( shouldBlurMobileComposerOnSubmit ( ) ) {
18311880 blurMobileComposerAfterSend ( ) ;
18321881 }
18331882 } ,
18341883 [
18351884 activeThreadId ,
1885+ activePendingProgress ,
18361886 blurMobileComposerAfterSend ,
18371887 isSendDisabled ,
18381888 noProviderAvailable ,
18391889 onSend ,
1890+ promptRef ,
18401891 shouldBlurMobileComposerOnSubmit ,
18411892 ] ,
18421893 ) ;
@@ -2590,6 +2641,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
25902641 selectedModel,
25912642 selectedProviderModels,
25922643 } ) ,
2644+ validateProviderInput : ( providerInput : string ) => {
2645+ const validationMessage = getComposerSubmissionValidationMessage ( {
2646+ prompt : promptRef . current ,
2647+ providerInput,
2648+ submissionTarget : "provider-turn" ,
2649+ } ) ;
2650+ providerInputRejectedRef . current = validationMessage !== null ;
2651+ setProviderInputSubmissionError ( validationMessage ) ;
2652+ return validationMessage === null ;
2653+ } ,
25932654 } ) ,
25942655 [
25952656 activeThread ,
@@ -3058,6 +3119,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
30583119 </ div >
30593120 </ div >
30603121
3122+ < ComposerPromptLengthValidation
3123+ message = { providerInputSubmissionError ?? composerSubmissionError }
3124+ />
3125+
30613126 { /* Bottom toolbar */ }
30623127 { isComposerCollapsedMobile ? null : activePendingApproval ? (
30633128 < div className = "flex items-center justify-end gap-2 px-3 pb-3 sm:px-4 sm:pb-4" >
0 commit comments