| .. | .. |
|---|
| 167 | 167 | // Per-session typing indicator (sessionId → boolean) |
|---|
| 168 | 168 | const typingMapRef = useRef<Record<string, boolean>>({}); |
|---|
| 169 | 169 | const [isTyping, setIsTyping] = useState(false); |
|---|
| 170 | | - // Toast for other-session incoming messages |
|---|
| 170 | + // Toast queue for other-session incoming messages (show one at a time) |
|---|
| 171 | + const toastQueueRef = useRef<{ sessionId: string; sessionName: string; preview: string }[]>([]); |
|---|
| 171 | 172 | const [incomingToast, setIncomingToast] = useState<{ sessionId: string; sessionName: string; preview: string } | null>(null); |
|---|
| 172 | 173 | // PAI projects list |
|---|
| 173 | 174 | const [projects, setProjects] = useState<PaiProject[]>([]); |
|---|
| .. | .. |
|---|
| 265 | 266 | ...u, |
|---|
| 266 | 267 | [sessionId]: (u[sessionId] ?? 0) + 1, |
|---|
| 267 | 268 | })); |
|---|
| 268 | | - // Show toast for other-session messages (assistant only, skip system noise) |
|---|
| 269 | + // Queue toast for other-session messages (assistant only, skip system noise) |
|---|
| 269 | 270 | if (msg.role === "assistant") { |
|---|
| 270 | 271 | setSessions((prev) => { |
|---|
| 271 | 272 | const session = prev.find((s) => s.id === sessionId); |
|---|
| 272 | 273 | const name = session?.name ?? sessionId.slice(0, 8); |
|---|
| 273 | 274 | const preview = msg.type === "voice" ? "🎤 Voice note" : msg.type === "image" ? "📷 Image" : (msg.content ?? "").slice(0, 60); |
|---|
| 274 | | - setIncomingToast({ sessionId, sessionName: name, preview }); |
|---|
| 275 | + const toast = { sessionId, sessionName: name, preview }; |
|---|
| 276 | + // If no toast is showing, show immediately; otherwise queue |
|---|
| 277 | + setIncomingToast((current) => { |
|---|
| 278 | + if (current === null) return toast; |
|---|
| 279 | + toastQueueRef.current.push(toast); |
|---|
| 280 | + return current; |
|---|
| 281 | + }); |
|---|
| 275 | 282 | return prev; |
|---|
| 276 | 283 | }); |
|---|
| 277 | 284 | } |
|---|
| .. | .. |
|---|
| 444 | 451 | status: "sending", |
|---|
| 445 | 452 | }; |
|---|
| 446 | 453 | addMessageToActive(msg); |
|---|
| 447 | | - const sent = wsSend(text); |
|---|
| 454 | + const sent = wsSend(text, activeSessionIdRef.current ?? undefined); |
|---|
| 448 | 455 | updateMessageStatus(id, sent ? "sent" : "error"); |
|---|
| 449 | 456 | }, |
|---|
| 450 | 457 | [wsSend, addMessageToActive, updateMessageStatus] |
|---|
| .. | .. |
|---|
| 466 | 473 | addMessageToActive(msg); |
|---|
| 467 | 474 | try { |
|---|
| 468 | 475 | const base64 = await encodeAudioToBase64(audioUri); |
|---|
| 469 | | - const sent = wsVoice(base64, "", id); |
|---|
| 476 | + const sent = wsVoice(base64, "", id, activeSessionIdRef.current ?? undefined); |
|---|
| 470 | 477 | updateMessageStatus(id, sent ? "sent" : "error"); |
|---|
| 471 | 478 | } catch (err) { |
|---|
| 472 | 479 | console.error("Failed to encode audio:", err); |
|---|
| .. | .. |
|---|
| 489 | 496 | status: "sending", |
|---|
| 490 | 497 | }; |
|---|
| 491 | 498 | addMessageToActive(msg); |
|---|
| 492 | | - const sent = wsImageSend(imageBase64, caption, mimeType); |
|---|
| 499 | + const sent = wsImageSend(imageBase64, caption, mimeType, activeSessionIdRef.current ?? undefined); |
|---|
| 493 | 500 | updateMessageStatus(id, sent ? "sent" : "error"); |
|---|
| 494 | 501 | }, |
|---|
| 495 | 502 | [wsImageSend, addMessageToActive, updateMessageStatus] |
|---|
| .. | .. |
|---|
| 560 | 567 | }, [sendCommand]); |
|---|
| 561 | 568 | |
|---|
| 562 | 569 | const dismissToast = useCallback(() => { |
|---|
| 563 | | - setIncomingToast(null); |
|---|
| 570 | + // Show next queued toast, or clear |
|---|
| 571 | + const next = toastQueueRef.current.shift(); |
|---|
| 572 | + setIncomingToast(next ?? null); |
|---|
| 564 | 573 | }, []); |
|---|
| 565 | 574 | |
|---|
| 566 | 575 | const loadMoreMessages = useCallback(() => { |
|---|