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 closeAgentEventsRef = 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 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 completeCanvasActionStream = useCallback(
@@ -49,10 +49,19 @@ export function useGenerationStream({
[setCanvasActionGenerating]
);
const completeImageGeneratorStream = useCallback(
(streamKey: string) => {
closeImageGeneratorEventsRef.current.delete(streamKey);
imageGeneratorStreamingProjectIdRef.current.delete(streamKey);
setImageGeneratorGenerating(closeImageGeneratorEventsRef.current.size > 0);
},
[setImageGeneratorGenerating]
);
const completeChannel = useCallback(
(channel: GenerationChannel) => {
if (channel === "image-generator") {
setImageGeneratorGenerating(false);
setImageGeneratorGenerating(closeImageGeneratorEventsRef.current.size > 0);
return;
}
setAgentGenerating(false);
@@ -66,12 +75,12 @@ export function useGenerationStream({
if (!threadId) return;
if (channel === "canvas-action") {
if (closeCanvasActionEventsRef.current.has(streamKey)) return;
closeCanvasActionEventsRef.current.get(streamKey)?.();
setCanvasActionGenerating(true);
canvasActionStreamingProjectIdRef.current.add(streamKey);
} else {
closeImageGeneratorEventsRef.current?.();
imageGeneratorStreamingProjectIdRef.current = streamKey;
closeImageGeneratorEventsRef.current.get(streamKey)?.();
imageGeneratorStreamingProjectIdRef.current.add(streamKey);
setImageGeneratorGenerating(true);
}
@@ -90,9 +99,7 @@ export function useGenerationStream({
completeCanvasActionStream(streamKey);
return;
}
completeChannel(channel);
imageGeneratorStreamingProjectIdRef.current = null;
closeImageGeneratorEventsRef.current = null;
completeImageGeneratorStream(streamKey);
};
const poll = () => {
void (async () => {
@@ -128,11 +135,11 @@ export function useGenerationStream({
if (channel === "canvas-action") {
closeCanvasActionEventsRef.current.set(streamKey, close);
} else {
closeImageGeneratorEventsRef.current = close;
closeImageGeneratorEventsRef.current.set(streamKey, close);
}
poll();
},
[applyProject, completeCanvasActionStream, completeChannel, toUserMessageText, messages.generateError, setCanvasActionGenerating, setError, setImageGeneratorGenerating]
[applyProject, completeCanvasActionStream, completeImageGeneratorStream, toUserMessageText, messages.generateError, setCanvasActionGenerating, setError, setImageGeneratorGenerating]
);
const startAgentProjectPolling = useCallback(
@@ -253,14 +260,24 @@ export function useGenerationStream({
(nextProjectId: string, payload: AgentChatPayload, channel: GenerationChannel = "agent") => {
const streamKey = generationStreamKey(nextProjectId, payload.threadId);
if (channel !== "agent") {
const closeRef = generationCloseRef(channel, closeAgentEventsRef, closeImageGeneratorEventsRef, closeCanvasActionEventsRef);
const streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef);
closeRef.current?.();
if (channel === "canvas-action") {
closeCanvasActionEventsRef.current.get(streamKey)?.();
setCanvasActionGenerating(true);
canvasActionStreamingProjectIdRef.current.add(streamKey);
} else {
closeImageGeneratorEventsRef.current.get(streamKey)?.();
setImageGeneratorGenerating(true);
imageGeneratorStreamingProjectIdRef.current.add(streamKey);
}
let closed = false;
closeRef.current = () => {
const close = () => {
closed = true;
};
streamingRef.current = streamKey;
if (channel === "canvas-action") {
closeCanvasActionEventsRef.current.set(streamKey, close);
} else {
closeImageGeneratorEventsRef.current.set(streamKey, close);
}
void designGateway
.agentChat(nextProjectId, payload)
.then((response) => {
@@ -270,10 +287,12 @@ export function useGenerationStream({
})
.catch((error: unknown) => {
if (closed) return;
completeChannel(channel);
if (channel === "canvas-action") {
completeCanvasActionStream(streamKey);
} else {
completeImageGeneratorStream(streamKey);
}
setError(toUserMessageText(error, "generateError"));
streamingRef.current = null;
closeRef.current = null;
});
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(
@@ -331,6 +350,13 @@ export function useGenerationStream({
setCanvasActionGenerating(false);
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 streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef);
closeRef.current?.();
@@ -342,21 +368,21 @@ export function useGenerationStream({
streamingRef.current = null;
completeChannel(channel);
},
[completeChannel, setCanvasActionGenerating]
[completeChannel, setCanvasActionGenerating, setImageGeneratorGenerating]
);
useEffect(() => {
return () => {
closeAgentEventsRef.current?.();
closeAgentProjectPollRef.current?.();
closeImageGeneratorEventsRef.current?.();
closeImageGeneratorEventsRef.current.forEach((close) => close());
closeCanvasActionEventsRef.current.forEach((close) => close());
closeAgentEventsRef.current = null;
closeAgentProjectPollRef.current = null;
closeImageGeneratorEventsRef.current = null;
closeImageGeneratorEventsRef.current.clear();
closeCanvasActionEventsRef.current.clear();
agentStreamingProjectIdRef.current = null;
imageGeneratorStreamingProjectIdRef.current = null;
imageGeneratorStreamingProjectIdRef.current.clear();
canvasActionStreamingProjectIdRef.current.clear();
};
}, []);
@@ -375,20 +401,18 @@ export function useGenerationStream({
function generationCloseRef(
channel: GenerationChannel,
agentRef: RefObject<(() => void) | null>,
imageGeneratorRef: RefObject<(() => void) | null>,
_imageGeneratorRef: RefObject<Map<string, () => void>>,
_canvasActionRef: RefObject<Map<string, () => void>>
) {
if (channel === "image-generator") return imageGeneratorRef;
return agentRef;
}
function generationStreamingRef(
channel: GenerationChannel,
agentRef: RefObject<string | null>,
imageGeneratorRef: RefObject<string | null>,
_imageGeneratorRef: RefObject<Set<string>>,
_canvasActionRef: RefObject<Set<string>>
) {
if (channel === "image-generator") return imageGeneratorRef;
return agentRef;
}