Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Web: Fix Enter key sending message during IME composition on Safari — Safari fires `compositionend` before the Enter `keydown`, making both `isComposing` guards false; adds a 100 ms post-composition cooldown to block premature submission (WebKit bug #165004)

- Core: Fix agent loop silently stopping when model response contains only thinking content — detect think-only responses (reasoning content with no text or tool calls) as an incomplete response error and retry automatically
- Core: Fix crash on streaming mid-flight network disconnection — when the OpenAI SDK raises a base `APIError` (instead of `APIConnectionError`) during long-running streams, the error is now correctly classified as retryable, enabling automatic retry and connection recovery instead of an unrecoverable crash
- Shell: Exclude empty current session from `/sessions` picker — completely empty sessions (no conversation history and no custom title) are no longer shown in the session list; sessions with a custom title are still displayed
Expand Down
16 changes: 14 additions & 2 deletions web/src/components/ai-elements/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,11 @@ export const PromptInputTextarea = forwardRef<
const controller = useOptionalPromptInputController();
const attachments = usePromptInputAttachments();
const [isComposing, setIsComposing] = useState(false);
// Track when compositionend last fired. Safari (WebKit bug #165004) fires
// compositionend *before* the Enter keydown, so both isComposing and
// nativeEvent.isComposing are false by the time we check. A short cooldown
// after compositionend prevents that Enter from triggering a send.
const compositionEndTimeRef = useRef(0);

const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
onKeyDown?.(e);
Expand All @@ -962,7 +967,11 @@ export const PromptInputTextarea = forwardRef<
}

if (e.key === "Enter") {
if (isComposing || e.nativeEvent.isComposing) {
if (
isComposing ||
e.nativeEvent.isComposing ||
e.timeStamp - compositionEndTimeRef.current < 100
) {
Comment on lines +970 to +974
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate composition cooldown so fast Enter still submits

The new Date.now() - compositionEndTimeRef.current < 100 guard in handleKeyDown runs on all browsers, not just Safari. After any compositionend (for example, IME text committed via Space/candidate click), a user who presses Enter quickly to send will hit this early return before e.preventDefault(), so the textarea inserts a newline instead of submitting. This creates a cross-browser regression for fast IME workflows; the cooldown should be limited to the Safari-specific path (or suppress default Enter behavior when blocking submit).

Useful? React with 👍 / 👎.

Comment on lines +970 to +974
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cooldown uses Date.now() deltas, which are based on handler execution time and wall-clock time. If the main thread is blocked (or the system clock changes), the computed delta can exceed 100ms (or go negative), defeating the safeguard or suppressing Enter unexpectedly. Consider storing the compositionend event timestamp (e.g., e.timeStamp or performance.now()) and comparing it to the keydown event’s timestamp instead of Date.now().

Copilot uses AI. Check for mistakes.
return;
}
if (e.shiftKey) {
Expand Down Expand Up @@ -1044,7 +1053,10 @@ export const PromptInputTextarea = forwardRef<
autoComplete="off"
name="message"
onBlur={() => setIsComposing(false)}
onCompositionEnd={() => setIsComposing(false)}
onCompositionEnd={(e) => {
setIsComposing(false);
compositionEndTimeRef.current = e.timeStamp;
}}
Comment on lines 1055 to +1059
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onBlur/onCompositionEnd are defined here but then can be overridden by {...props} later in the JSX spread. In current usage (ChatPromptComposer) onBlur is passed, so setIsComposing(false) never runs on blur; similarly, any future onCompositionEnd prop would bypass updating compositionEndTimeRef and reintroduce the Safari bug. Consider destructuring onBlur/onCompositionStart/onCompositionEnd from props and invoking them inside internal handlers (or spread props before attaching these internal handlers) so internal composition state and the cooldown ref are always maintained.

Copilot uses AI. Check for mistakes.
onCompositionStart={() => setIsComposing(true)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
Expand Down
Loading