refactor(canvas): support multiple concurrent image generator streams

Track image-generator streams in a keyed Map/Set instead of a single ref
so several placeholders can generate at once without cancelling each
other. Generating state is derived from the number of open streams.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 01:46:07 +08:00
parent 453de420fc
commit 82683c40ea
@@ -34,10 +34,10 @@ export function useGenerationStream({
const toUserMessageText = useUserMessage(); const toUserMessageText = useUserMessage();
const closeAgentEventsRef = useRef<(() => void) | null>(null); const closeAgentEventsRef = useRef<(() => void) | null>(null);
const closeAgentProjectPollRef = useRef<(() => void) | null>(null); const closeAgentProjectPollRef = useRef<(() => void) | null>(null);
const closeImageGeneratorEventsRef = useRef<(() => void) | null>(null); const closeImageGeneratorEventsRef = useRef<Map<string, () => void>>(new Map());
const closeCanvasActionEventsRef = useRef<Map<string, () => void>>(new Map()); const closeCanvasActionEventsRef = useRef<Map<string, () => void>>(new Map());
const agentStreamingProjectIdRef = useRef<string | null>(null); const agentStreamingProjectIdRef = useRef<string | null>(null);
const imageGeneratorStreamingProjectIdRef = useRef<string | null>(null); const imageGeneratorStreamingProjectIdRef = useRef<Set<string>>(new Set());
const canvasActionStreamingProjectIdRef = useRef<Set<string>>(new Set()); const canvasActionStreamingProjectIdRef = useRef<Set<string>>(new Set());
const completeCanvasActionStream = useCallback( const completeCanvasActionStream = useCallback(
@@ -49,10 +49,19 @@ export function useGenerationStream({
[setCanvasActionGenerating] [setCanvasActionGenerating]
); );
const completeImageGeneratorStream = useCallback(
(streamKey: string) => {
closeImageGeneratorEventsRef.current.delete(streamKey);
imageGeneratorStreamingProjectIdRef.current.delete(streamKey);
setImageGeneratorGenerating(closeImageGeneratorEventsRef.current.size > 0);
},
[setImageGeneratorGenerating]
);
const completeChannel = useCallback( const completeChannel = useCallback(
(channel: GenerationChannel) => { (channel: GenerationChannel) => {
if (channel === "image-generator") { if (channel === "image-generator") {
setImageGeneratorGenerating(false); setImageGeneratorGenerating(closeImageGeneratorEventsRef.current.size > 0);
return; return;
} }
setAgentGenerating(false); setAgentGenerating(false);
@@ -66,12 +75,12 @@ export function useGenerationStream({
if (!threadId) return; if (!threadId) return;
if (channel === "canvas-action") { if (channel === "canvas-action") {
if (closeCanvasActionEventsRef.current.has(streamKey)) return; closeCanvasActionEventsRef.current.get(streamKey)?.();
setCanvasActionGenerating(true); setCanvasActionGenerating(true);
canvasActionStreamingProjectIdRef.current.add(streamKey); canvasActionStreamingProjectIdRef.current.add(streamKey);
} else { } else {
closeImageGeneratorEventsRef.current?.(); closeImageGeneratorEventsRef.current.get(streamKey)?.();
imageGeneratorStreamingProjectIdRef.current = streamKey; imageGeneratorStreamingProjectIdRef.current.add(streamKey);
setImageGeneratorGenerating(true); setImageGeneratorGenerating(true);
} }
@@ -90,9 +99,7 @@ export function useGenerationStream({
completeCanvasActionStream(streamKey); completeCanvasActionStream(streamKey);
return; return;
} }
completeChannel(channel); completeImageGeneratorStream(streamKey);
imageGeneratorStreamingProjectIdRef.current = null;
closeImageGeneratorEventsRef.current = null;
}; };
const poll = () => { const poll = () => {
void (async () => { void (async () => {
@@ -128,11 +135,11 @@ export function useGenerationStream({
if (channel === "canvas-action") { if (channel === "canvas-action") {
closeCanvasActionEventsRef.current.set(streamKey, close); closeCanvasActionEventsRef.current.set(streamKey, close);
} else { } else {
closeImageGeneratorEventsRef.current = close; closeImageGeneratorEventsRef.current.set(streamKey, close);
} }
poll(); poll();
}, },
[applyProject, completeCanvasActionStream, completeChannel, toUserMessageText, messages.generateError, setCanvasActionGenerating, setError, setImageGeneratorGenerating] [applyProject, completeCanvasActionStream, completeImageGeneratorStream, toUserMessageText, messages.generateError, setCanvasActionGenerating, setError, setImageGeneratorGenerating]
); );
const startAgentProjectPolling = useCallback( const startAgentProjectPolling = useCallback(
@@ -253,14 +260,24 @@ export function useGenerationStream({
(nextProjectId: string, payload: AgentChatPayload, channel: GenerationChannel = "agent") => { (nextProjectId: string, payload: AgentChatPayload, channel: GenerationChannel = "agent") => {
const streamKey = generationStreamKey(nextProjectId, payload.threadId); const streamKey = generationStreamKey(nextProjectId, payload.threadId);
if (channel !== "agent") { if (channel !== "agent") {
const closeRef = generationCloseRef(channel, closeAgentEventsRef, closeImageGeneratorEventsRef, closeCanvasActionEventsRef); if (channel === "canvas-action") {
const streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef); closeCanvasActionEventsRef.current.get(streamKey)?.();
closeRef.current?.(); setCanvasActionGenerating(true);
canvasActionStreamingProjectIdRef.current.add(streamKey);
} else {
closeImageGeneratorEventsRef.current.get(streamKey)?.();
setImageGeneratorGenerating(true);
imageGeneratorStreamingProjectIdRef.current.add(streamKey);
}
let closed = false; let closed = false;
closeRef.current = () => { const close = () => {
closed = true; closed = true;
}; };
streamingRef.current = streamKey; if (channel === "canvas-action") {
closeCanvasActionEventsRef.current.set(streamKey, close);
} else {
closeImageGeneratorEventsRef.current.set(streamKey, close);
}
void designGateway void designGateway
.agentChat(nextProjectId, payload) .agentChat(nextProjectId, payload)
.then((response) => { .then((response) => {
@@ -270,10 +287,12 @@ export function useGenerationStream({
}) })
.catch((error: unknown) => { .catch((error: unknown) => {
if (closed) return; if (closed) return;
completeChannel(channel); if (channel === "canvas-action") {
completeCanvasActionStream(streamKey);
} else {
completeImageGeneratorStream(streamKey);
}
setError(toUserMessageText(error, "generateError")); setError(toUserMessageText(error, "generateError"));
streamingRef.current = null;
closeRef.current = null;
}); });
return; return;
} }
@@ -319,7 +338,7 @@ export function useGenerationStream({
} }
}); });
}, },
[appendThinkingMessage, applyProject, completeChannel, toUserMessageText, messages.generateError, messages.generateTimeout, setError, startAgentProjectPolling, startGeneratorTaskPolling] [appendThinkingMessage, applyProject, completeCanvasActionStream, completeChannel, completeImageGeneratorStream, toUserMessageText, messages.generateError, messages.generateTimeout, setCanvasActionGenerating, setError, setImageGeneratorGenerating, startAgentProjectPolling, startGeneratorTaskPolling]
); );
const stopGenerationStream = useCallback( const stopGenerationStream = useCallback(
@@ -331,6 +350,13 @@ export function useGenerationStream({
setCanvasActionGenerating(false); setCanvasActionGenerating(false);
return; return;
} }
if (channel === "image-generator") {
closeImageGeneratorEventsRef.current.forEach((close) => close());
closeImageGeneratorEventsRef.current.clear();
imageGeneratorStreamingProjectIdRef.current.clear();
setImageGeneratorGenerating(false);
return;
}
const closeRef = generationCloseRef(channel, closeAgentEventsRef, closeImageGeneratorEventsRef, closeCanvasActionEventsRef); const closeRef = generationCloseRef(channel, closeAgentEventsRef, closeImageGeneratorEventsRef, closeCanvasActionEventsRef);
const streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef); const streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef);
closeRef.current?.(); closeRef.current?.();
@@ -342,21 +368,21 @@ export function useGenerationStream({
streamingRef.current = null; streamingRef.current = null;
completeChannel(channel); completeChannel(channel);
}, },
[completeChannel, setCanvasActionGenerating] [completeChannel, setCanvasActionGenerating, setImageGeneratorGenerating]
); );
useEffect(() => { useEffect(() => {
return () => { return () => {
closeAgentEventsRef.current?.(); closeAgentEventsRef.current?.();
closeAgentProjectPollRef.current?.(); closeAgentProjectPollRef.current?.();
closeImageGeneratorEventsRef.current?.(); closeImageGeneratorEventsRef.current.forEach((close) => close());
closeCanvasActionEventsRef.current.forEach((close) => close()); closeCanvasActionEventsRef.current.forEach((close) => close());
closeAgentEventsRef.current = null; closeAgentEventsRef.current = null;
closeAgentProjectPollRef.current = null; closeAgentProjectPollRef.current = null;
closeImageGeneratorEventsRef.current = null; closeImageGeneratorEventsRef.current.clear();
closeCanvasActionEventsRef.current.clear(); closeCanvasActionEventsRef.current.clear();
agentStreamingProjectIdRef.current = null; agentStreamingProjectIdRef.current = null;
imageGeneratorStreamingProjectIdRef.current = null; imageGeneratorStreamingProjectIdRef.current.clear();
canvasActionStreamingProjectIdRef.current.clear(); canvasActionStreamingProjectIdRef.current.clear();
}; };
}, []); }, []);
@@ -375,20 +401,18 @@ export function useGenerationStream({
function generationCloseRef( function generationCloseRef(
channel: GenerationChannel, channel: GenerationChannel,
agentRef: RefObject<(() => void) | null>, agentRef: RefObject<(() => void) | null>,
imageGeneratorRef: RefObject<(() => void) | null>, _imageGeneratorRef: RefObject<Map<string, () => void>>,
_canvasActionRef: RefObject<Map<string, () => void>> _canvasActionRef: RefObject<Map<string, () => void>>
) { ) {
if (channel === "image-generator") return imageGeneratorRef;
return agentRef; return agentRef;
} }
function generationStreamingRef( function generationStreamingRef(
channel: GenerationChannel, channel: GenerationChannel,
agentRef: RefObject<string | null>, agentRef: RefObject<string | null>,
imageGeneratorRef: RefObject<string | null>, _imageGeneratorRef: RefObject<Set<string>>,
_canvasActionRef: RefObject<Set<string>> _canvasActionRef: RefObject<Set<string>>
) { ) {
if (channel === "image-generator") return imageGeneratorRef;
return agentRef; return agentRef;
} }