From 0b128c3a3463ca7d086742814751874b2d85ca51 Mon Sep 17 00:00:00 2001 From: "wizzoapp[bot]" <254688279+wizzoapp[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:09:03 +0100 Subject: [PATCH] fix(sync): address Codex findings in merge-resolved composer code Both findings land on resolution decisions made in this sync, in files from the conflicted-file set. - Offline composing (P2): adopting upstream's composer `disabled` guard locked the editor whenever the environment was unavailable. This fork deliberately queues sends made while disconnected (`shouldQueueInitialSend = activeEnvironmentUnavailable || ...`), so the user must be able to type the message that gets queued. Dropped the environment clause from both places the guard was adopted -- the `disabled` prop and the `insertTextAtEnd` guard -- while keeping upstream's new `projectSelectionRequired` guard. - Coarse-pointer Enter (P3): this sync retired the fork-local `hasCoarsePointer` helper for upstream's viewport-only one, which made plain Enter submit from an on-screen keyboard on tablets and 2-in-1s in landscape. Folded pointer capability into the shared helper as an optional input, so upstream's callers and tests are unaffected, and covered both wide-touch and wide-fine-pointer cases. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/components/chat/ChatComposer.tsx | 16 +++++++-------- apps/web/src/composer-logic.test.ts | 20 +++++++++++++++++++ apps/web/src/composer-logic.ts | 6 +++++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 181d02a2aba..7d278c2ce48 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -916,6 +916,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false); const [isComposerFocused, setIsComposerFocused] = useState(false); const isMobileViewport = useMediaQuery("max-sm"); + const hasCoarsePointer = useMediaQuery({ pointer: "coarse" }); const isComposerCollapsedMobile = isMobileViewport && !forceExpandedOnMobile && !isComposerFocused; @@ -1800,7 +1801,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) } if ( key === "Enter" && - shouldSubmitComposerOnEnter({ isMobileViewport, shiftKey: event.shiftKey }) + shouldSubmitComposerOnEnter({ isMobileViewport, hasCoarsePointer, shiftKey: event.shiftKey }) ) { submitComposer(); return true; @@ -2006,8 +2007,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) isConnecting || isComposerApprovalState || pendingUserInputs.length > 0 || - projectSelectionRequired || - (environmentUnavailable !== null && activePendingProgress === null) + projectSelectionRequired ) { return false; } @@ -2542,12 +2542,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ? "Ask for follow-up changes or attach images" : "Ask anything, @tag files/folders, $use skills, or / for commands" } - disabled={ - isConnecting || - isComposerApprovalState || - projectSelectionRequired || - (environmentUnavailable !== null && activePendingProgress === null) - } + // An unavailable environment must not lock the editor: this fork + // queues sends made while disconnected (`shouldQueueInitialSend`), + // so the user has to be able to type the message that gets queued. + disabled={isConnecting || isComposerApprovalState || projectSelectionRequired} /> {showMobilePendingAnswerActions ? (
{ it("inserts a newline for Shift+Enter", () => { expect(shouldSubmitComposerOnEnter({ isMobileViewport: false, shiftKey: true })).toBe(false); }); + + it("inserts a newline on a wide touch device, where Enter comes from an on-screen keyboard", () => { + expect( + shouldSubmitComposerOnEnter({ + isMobileViewport: false, + hasCoarsePointer: true, + shiftKey: false, + }), + ).toBe(false); + }); + + it("still submits on a wide fine-pointer device", () => { + expect( + shouldSubmitComposerOnEnter({ + isMobileViewport: false, + hasCoarsePointer: false, + shiftKey: false, + }), + ).toBe(true); + }); }); describe("detectComposerTrigger", () => { diff --git a/apps/web/src/composer-logic.ts b/apps/web/src/composer-logic.ts index 2d1d3aed3b1..d5c06d1f40c 100644 --- a/apps/web/src/composer-logic.ts +++ b/apps/web/src/composer-logic.ts @@ -11,11 +11,15 @@ export interface ComposerTrigger { rangeEnd: number; } +// A coarse pointer means an on-screen keyboard, where Enter has to insert a +// newline rather than send — the viewport check alone misses tablets and +// 2-in-1s held in landscape, whose viewport is wider than the mobile breakpoint. export function shouldSubmitComposerOnEnter(input: { isMobileViewport: boolean; shiftKey: boolean; + hasCoarsePointer?: boolean; }): boolean { - return !input.isMobileViewport && !input.shiftKey; + return !input.isMobileViewport && !input.hasCoarsePointer && !input.shiftKey; } const isInlineTokenSegment = (