fix(design): separate progress from chat replies

This commit is contained in:
2026-09-04 08:22:32 +08:00
parent 945b40de11
commit c6b0490d7f
5 changed files with 293 additions and 15 deletions

View File

@@ -96,7 +96,9 @@ type ImageWorkspaceState = {
let inFlightLoad: Promise<DesignWorkspaceBootstrap | null> | null = null;
let activeEventSource: EventSource | null = null;
let activeEventWorkspaceId: string | null = null;
let eventConnectionGeneration = 0;
let selectionGeneration = 0;
const assistantStreamChunkIndexes = new Map<string, number>();
function updateBackgroundLease(tasks: DesignGenerationTask[]): void {
const active = tasks.some((task) => task.status === 'queued' || task.status === 'running');
@@ -112,6 +114,7 @@ function updateBackgroundLease(tasks: DesignGenerationTask[]): void {
}
function closeEventSource(): void {
eventConnectionGeneration += 1;
if (activeEventSource) {
activeEventSource.onopen = null;
activeEventSource.onerror = null;
@@ -292,6 +295,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
}));
try {
const result = await submitImageWorkspaceCommand(command);
assistantStreamChunkIndexes.delete(operation.id);
set((state) => {
const pendingOperations = { ...state.pendingOperations };
delete pendingOperations[operation.id];
@@ -339,6 +343,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
return { pendingOperations, error: null };
});
} else if (isDefinitiveCommandFailure(error)) {
assistantStreamChunkIndexes.delete(operation.id);
set((state) => {
const pendingOperations = { ...state.pendingOperations };
delete pendingOperations[operation.id];
@@ -426,10 +431,12 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
const workspace = await createImageWorkspaceProject(title);
selectionGeneration += 1;
closeEventSource();
assistantStreamChunkIndexes.clear();
set((state) => ({
...withWorkspace(state, workspace),
fieldDrafts: {},
chatDraft: '',
assistantStreams: {},
quoteBlockers: [],
lastEventId: null,
}));
@@ -467,6 +474,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
if (deletingActive) {
selectionGeneration += 1;
closeEventSource();
assistantStreamChunkIndexes.clear();
}
set((state) => ({
bootstrap: state.bootstrap ? { ...state.bootstrap, workspaces: remaining } : null,
@@ -475,6 +483,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
deletingWorkspaceId: null,
fieldDrafts: deletingActive ? {} : state.fieldDrafts,
chatDraft: deletingActive ? '' : state.chatDraft,
assistantStreams: deletingActive ? {} : state.assistantStreams,
error: null,
}));
if (deletingActive && remaining[0]) {
@@ -492,6 +501,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
if (workspaceId === get().activeWorkspaceId && get().workspace) return;
selectionGeneration += 1;
closeEventSource();
assistantStreamChunkIndexes.clear();
set({
activeWorkspaceId: workspaceId,
workspace: null,
@@ -499,6 +509,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
lastEventId: null,
fieldDrafts: {},
chatDraft: '',
assistantStreams: {},
quoteBlockers: [],
error: null,
});
@@ -520,8 +531,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
const workspace = get().workspace;
if (!workspace) return;
const workspaceId = workspace.workspace.workspaceId;
if (activeEventSource && activeEventWorkspaceId === workspaceId) return;
if (activeEventWorkspaceId === workspaceId) return;
closeEventSource();
const connectionGeneration = eventConnectionGeneration;
activeEventWorkspaceId = workspaceId;
set({ eventState: 'connecting' });
void openImageWorkspaceEvents(
@@ -529,7 +541,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
workspace.workspace.sessionId,
get().lastEventId ?? undefined,
).then((source) => {
if (activeEventWorkspaceId !== workspaceId || get().activeWorkspaceId !== workspaceId) {
if (connectionGeneration !== eventConnectionGeneration
|| activeEventWorkspaceId !== workspaceId
|| get().activeWorkspaceId !== workspaceId) {
source.close();
return;
}
@@ -543,6 +557,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
const event = parseWorkspaceEvent(raw);
if (!event || get().activeWorkspaceId !== workspaceId) return;
if (event.type === 'design.assistant.delta') {
const previousChunkIndex = assistantStreamChunkIndexes.get(event.clientOperationId);
if (previousChunkIndex !== undefined && event.chunkIndex <= previousChunkIndex) return;
assistantStreamChunkIndexes.set(event.clientOperationId, event.chunkIndex);
set((state) => ({
lastEventId: event.id,
assistantStreams: {
@@ -592,7 +609,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
'design.workspace.updated',
]) source.addEventListener(eventType, receive);
}).catch((error) => {
if (activeEventWorkspaceId === workspaceId) {
if (connectionGeneration === eventConnectionGeneration
&& activeEventWorkspaceId === workspaceId) {
activeEventWorkspaceId = null;
set({ eventState: 'degraded', error: messageForError(error) });
}
});
@@ -708,6 +727,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
reset: () => {
selectionGeneration += 1;
closeEventSource();
assistantStreamChunkIndexes.clear();
updateBackgroundLease([]);
set({
status: 'idle',