fix(frontend): block duplicate sends during uploads (#1165)

* fix(frontend): block duplicate sends during uploads

Expose pre-submit upload work as a busy state so the chat input does not allow a second send while the first attachment is still uploading.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs(frontend): document upload and stream ownership

Record that thread hooks own upload-before-submit state while the chat page owns composer busy wiring, so future changes do not reintroduce duplicate socket or upload state handling.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(frontend): separate upload busy state from streaming

Keep uploads from reusing the streaming stop state so duplicate submits are blocked without turning the composer into a stop button during file uploads.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Ryanba
2026-03-18 15:10:27 +08:00
committed by GitHub
parent beb0eab711
commit f737fbeae8
3 changed files with 22 additions and 3 deletions

View File

@@ -156,6 +156,8 @@ export function useThreadStream({
// Optimistic messages shown before the server stream responds
const [optimisticMessages, setOptimisticMessages] = useState<Message[]>([]);
const [isUploading, setIsUploading] = useState(false);
const sendInFlightRef = useRef(false);
// Track message count before sending so we know when server has responded
const prevMsgCountRef = useRef(thread.messages.length);
@@ -175,6 +177,11 @@ export function useThreadStream({
message: PromptInputMessage,
extraContext?: Record<string, unknown>,
) => {
if (sendInFlightRef.current) {
return;
}
sendInFlightRef.current = true;
const text = message.text.trim();
// Capture current count before showing optimistic messages
@@ -217,6 +224,7 @@ export function useThreadStream({
try {
// Upload files first if any
if (message.files && message.files.length > 0) {
setIsUploading(true);
try {
// Convert FileUIPart to File objects by fetching blob URLs
const filePromises = message.files.map(async (fileUIPart) => {
@@ -293,6 +301,8 @@ export function useThreadStream({
toast.error(errorMessage);
setOptimisticMessages([]);
throw error;
} finally {
setIsUploading(false);
}
}
@@ -342,7 +352,10 @@ export function useThreadStream({
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
} catch (error) {
setOptimisticMessages([]);
setIsUploading(false);
throw error;
} finally {
sendInFlightRef.current = false;
}
},
[thread, _handleOnStart, t.uploads.uploadingFiles, context, queryClient],
@@ -357,7 +370,7 @@ export function useThreadStream({
} as typeof thread)
: thread;
return [mergedThread, sendMessage] as const;
return [mergedThread, sendMessage, isUploading] as const;
}
export function useThreads(