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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user