fix(dashboard): self-clear mobile send/stop dedupe ref so subsequent taps work

The previous fix introduced a regression: handledMobileSendRef /
handledMobileActionRef was set in onPointerDown/onTouchStart but only
cleared in onClick. preventDefault() on pointer/touch events suppresses
the synthesized click on iOS Safari, so the ref stayed true forever
after the first tap — every subsequent tap (quick OR long press) hit
the new dedupe guard and silently bailed.

Now both handlers schedule a 500ms setTimeout to self-clear the ref
alongside their action. That covers the full pointerdown/touchstart/
click burst from one tap while still letting the next user tap go
through.

Applied to: ChatView send button, QuickChat send button, QuickChat
stop button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-06 19:59:38 -07:00
parent 35ea499e08
commit 157865a583
2 changed files with 26 additions and 16 deletions

View File

@@ -1998,22 +1998,21 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
if (typeof window === "undefined" || window.innerWidth > 768) return;
event.preventDefault();
if (event.pointerType && event.pointerType !== "mouse") {
// On mobile, both onPointerDown and onTouchStart fire
// for a quick tap. Without this guard handleSend runs
// twice — the second call closes the first's stream
// (useChat.ts: streamRef.current.close()) and the
// server-side beginGeneration aborts the in-flight
// generation, leaving the chat with no output. A
// long-press doesn't fire both events the same way,
// which is why holding the button "fixes" sending.
// Dedupe across the pointerdown/touchstart/click burst
// from a single tap. preventDefault() on pointer/touch
// can suppress the synthesized click that previously
// cleared this ref, so we self-clear it via a short
// timeout — otherwise once set the next tap silently
// bails (long press stops working too).
if (handledMobileSendRef.current) return;
handledMobileSendRef.current = true;
markPreserveComposerFocus();
focusComposerInput();
void handleSend();
window.setTimeout(() => {
handledMobileSendRef.current = false;
preserveComposerFocusRef.current = false;
}, 1500);
}, 500);
}
}}
onTouchStart={(event) => {
@@ -2025,8 +2024,9 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
focusComposerInput();
void handleSend();
window.setTimeout(() => {
handledMobileSendRef.current = false;
preserveComposerFocusRef.current = false;
}, 1500);
}, 500);
}}
onMouseDown={(event) => {
if (typeof window === "undefined" || window.innerWidth > 768) return;

View File

@@ -2474,6 +2474,9 @@ export function QuickChatFAB({
if (handledMobileActionRef.current) return;
handledMobileActionRef.current = true;
stopStreaming();
window.setTimeout(() => {
handledMobileActionRef.current = false;
}, 500);
}
}}
onTouchStart={(event) => {
@@ -2482,6 +2485,9 @@ export function QuickChatFAB({
if (handledMobileActionRef.current) return;
handledMobileActionRef.current = true;
stopStreaming();
window.setTimeout(() => {
handledMobileActionRef.current = false;
}, 500);
}}
onMouseDown={(event) => {
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
@@ -2507,17 +2513,18 @@ export function QuickChatFAB({
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
event.preventDefault();
if (event.pointerType && event.pointerType !== "mouse") {
// Guard against the same touch firing both
// onPointerDown and onTouchStart, which would call
// handleSendMessage twice — the second call closes
// the first's stream and the server-side
// beginGeneration aborts the in-flight generation,
// producing no output. (See ChatView send button.)
// Dedupe across pointerdown/touchstart/click for one
// tap; self-clear via setTimeout because preventDefault
// can suppress the synthesized click that previously
// cleared this ref. (See ChatView send button.)
if (handledMobileActionRef.current) return;
handledMobileActionRef.current = true;
markPreserveComposerFocus();
focusComposerInput();
void handleSendMessage();
window.setTimeout(() => {
handledMobileActionRef.current = false;
}, 500);
}
}}
onTouchStart={(event) => {
@@ -2528,6 +2535,9 @@ export function QuickChatFAB({
markPreserveComposerFocus();
focusComposerInput();
void handleSendMessage();
window.setTimeout(() => {
handledMobileActionRef.current = false;
}, 500);
}}
onMouseDown={(event) => {
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;