Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5.2k
fix(web): reject oversized prompts before provider turn start#6602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -106,6 +106,12 @@ import { buildExpandedImagePreview, type ExpandedImagePreview } from "./Expanded | ||
| import { basenameOfPath } from "../../pierre-icons"; | ||
| import { cn, randomUUID } from "~/lib/utils"; | ||
| import { Separator } from "../ui/separator"; | ||
| import { | ||
| getComposerPromptLengthValidationMessage, | ||
| getComposerSubmissionValidationMessage, | ||
| submitComposerDraft, | ||
| } from "./composerSubmission"; | ||
| import { ComposerPromptLengthValidation } from "./ComposerPromptLengthValidation"; | ||
| type ComposerCommandMenuPosition = { | ||
| bottom: number; | ||
| @@ -484,6 +490,8 @@ export interface ChatComposerHandle { | ||
| selectedModel: string; | ||
| selectedProviderModels: ReadonlyArray<ServerProvider["models"][number]>; | ||
| }; | ||
| /** Validate the fully composed text immediately before a provider turn starts. */ | ||
| validateProviderInput: (providerInput: string) => boolean; | ||
| } | ||
| // -------------------------------------------------------------------------- | ||
| @@ -947,6 +955,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ||
| const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false); | ||
| const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false); | ||
| const [isComposerFocused, setIsComposerFocused] = useState(false); | ||
| const [composerSubmissionError, setComposerSubmissionError] = useState<string | null>(null); | ||
| const [providerInputSubmissionError, setProviderInputSubmissionError] = useState<string | null>( | ||
| null, | ||
| ); | ||
| const [composerMenuAnchor, setComposerMenuAnchor] = useState<HTMLDivElement | null>(null); | ||
| const [isStashMenuOpen, setIsStashMenuOpen] = useState(false); | ||
| const [stashPulse, setStashPulse] = useState<{ key: number; active: boolean }>({ | ||
| @@ -963,6 +975,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ||
| const composerEditorRef = useRef<ComposerPromptEditorHandle>(null); | ||
| const composerFormRef = useRef<HTMLFormElement>(null); | ||
| const composerSurfaceRef = useRef<HTMLDivElement>(null); | ||
| const providerInputRejectedRef = useRef(false); | ||
| const composerSelectLockRef = useRef(false); | ||
| const composerMenuOpenRef = useRef(false); | ||
| const composerMenuItemsRef = useRef<ComposerCommandItem[]>([]); | ||
| @@ -1306,6 +1319,27 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ||
| setComposerCursor((existing) => clampCollapsedComposerCursor(prompt, existing)); | ||
| }, [prompt, promptRef]); | ||
| useEffect(() => { | ||
| if (composerSubmissionError === null) return; | ||
| const nextError = getComposerPromptLengthValidationMessage(prompt); | ||
| if (nextError !== composerSubmissionError) { | ||
| setComposerSubmissionError(nextError); | ||
| } | ||
| }, [composerSubmissionError, prompt]); | ||
| useEffect(() => { | ||
| setProviderInputSubmissionError(null); | ||
| }, [ | ||
| composerElementContexts, | ||
| composerPreviewAnnotations, | ||
| composerReviewComments, | ||
| composerTerminalContexts, | ||
| prompt, | ||
| selectedModel, | ||
| selectedPromptEffort, | ||
| selectedProvider, | ||
| ]); | ||
| useEffect(() => { | ||
| composerImagesRef.current = composerImages; | ||
| }, [composerImages, composerImagesRef]); | ||
| @@ -1397,6 +1431,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ||
| // ------------------------------------------------------------------ | ||
| useEffect(() => { | ||
| setComposerHighlightedItemId(null); | ||
| setComposerSubmissionError(null); | ||
| setProviderInputSubmissionError(null); | ||
naveed949 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| setComposerCursor(collapseExpandedComposerCursor(promptRef.current, promptRef.current.length)); | ||
| setComposerTrigger(detectComposerTrigger(promptRef.current, promptRef.current.length)); | ||
| dragDepthRef.current = 0; | ||
| @@ -1824,17 +1860,32 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ||
| }); | ||
| return; | ||
| } | ||
| onSend(event); | ||
| const submission = submitComposerDraft({ | ||
| prompt: promptRef.current, | ||
macroscopeapp[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| submissionTarget: activePendingProgress ? "pending-user-input" : "provider-turn", | ||
| event, | ||
| onSend: (sendEvent) => { | ||
| // ChatView reports its final composed-input preflight through the | ||
| // composer handle before its first asynchronous send step. | ||
| providerInputRejectedRef.current = false; | ||
| onSend(sendEvent); | ||
| return !providerInputRejectedRef.current; | ||
| }, | ||
| }); | ||
| setComposerSubmissionError(submission.validationMessage); | ||
| if (!submission.didDispatch) return; | ||
| if (shouldBlurMobileComposerOnSubmit()) { | ||
| blurMobileComposerAfterSend(); | ||
| } | ||
| }, | ||
| [ | ||
| activeThreadId, | ||
| activePendingProgress, | ||
| blurMobileComposerAfterSend, | ||
| isSendDisabled, | ||
| noProviderAvailable, | ||
| onSend, | ||
| promptRef, | ||
| shouldBlurMobileComposerOnSubmit, | ||
| ], | ||
| ); | ||
| @@ -2616,6 +2667,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ||
| selectedModel, | ||
| selectedProviderModels, | ||
| }), | ||
| validateProviderInput: (providerInput: string) => { | ||
| const validationMessage = getComposerSubmissionValidationMessage({ | ||
| prompt: promptRef.current, | ||
| providerInput, | ||
| submissionTarget: "provider-turn", | ||
| }); | ||
| providerInputRejectedRef.current = validationMessage !== null; | ||
| setProviderInputSubmissionError(validationMessage); | ||
| return validationMessage === null; | ||
| }, | ||
| }), | ||
| [ | ||
| activeThread, | ||
| @@ -3086,6 +3147,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ||
| </div> | ||
| </div> | ||
| <ComposerPromptLengthValidation | ||
| message={providerInputSubmissionError ?? composerSubmissionError} | ||
| /> | ||
| {/* Bottom toolbar */} | ||
| {isComposerCollapsedMobile ? null : activePendingApproval ? ( | ||
| <div className="flex items-center justify-end gap-2 px-3 pb-3 sm:px-4 sm:pb-4"> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { PROVIDER_SEND_TURN_MAX_INPUT_CHARS } from "@t3tools/contracts"; | ||
| import { renderToStaticMarkup } from "react-dom/server"; | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
| import { getComposerPromptLengthValidationMessage } from "./composerSubmission"; | ||
| import { ComposerPromptLengthValidation } from "./ComposerPromptLengthValidation"; | ||
| describe("ComposerPromptLengthValidation", () => { | ||
| it("renders oversized prompt feedback as an actionable composer alert", () => { | ||
| const message = getComposerPromptLengthValidationMessage( | ||
| "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS + 1), | ||
| ); | ||
| const markup = renderToStaticMarkup(<ComposerPromptLengthValidation message={message} />); | ||
| expect(markup).toContain('role="alert"'); | ||
| expect(markup).toContain('data-chat-composer-validation="prompt-length"'); | ||
| expect(markup).toContain( | ||
| "Prompt is 1 character over the 120,000-character limit. Shorten or split it before sending.", | ||
| ); | ||
| expect(markup).not.toContain("ProviderValidationError"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export function ComposerPromptLengthValidation({ message }: { message: string | null }) { | ||
| if (!message) return null; | ||
| return ( | ||
| <p | ||
| role="alert" | ||
| className="px-3 pb-2 text-xs text-destructive sm:px-4" | ||
| data-chat-composer-validation="prompt-length" | ||
| > | ||
| {message} | ||
| </p> | ||
| ); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.