fix(coding): preserve renderer recovery state

This commit is contained in:
2026-08-23 22:27:21 +08:00
parent 36626c88b5
commit 1abb2fff36
3 changed files with 125 additions and 7 deletions

View File

@@ -216,6 +216,27 @@ function withRejectedNode(
});
}
function withUnreconciledOptimisticNodes(
reducer: ConversationReducerState,
requests: Record<string, CodingPromptRequestState>,
): ConversationReducerState {
const snapshot = reducer.snapshot;
if (!snapshot) return reducer;
const existingRequestIds = requestIdsInSnapshot(snapshot);
const nodes = Object.values(requests).flatMap((request) => (
request.nodeId
&& request.status !== 'rejected'
&& !existingRequestIds.has(request.clientRequestId)
? [optimisticNode(request.nodeId, request.clientRequestId, request.submittedDraft)]
: []
));
if (nodes.length === 0) return reducer;
return replaceConversationSnapshot(reducer, {
...snapshot,
nodes: [...snapshot.nodes, ...nodes],
});
}
function snapshotIsOlder(
current: ConversationSnapshot | null,
incoming: ConversationSnapshot,
@@ -241,11 +262,33 @@ export function createCodingConversationStore(
): StoreApi<CodingConversationStoreState> {
const deps = { ...defaultDependencies(), ...dependencies };
const snapshotLoads = new Map<string, Promise<ConversationSnapshot>>();
const recoveryPatches = new Map<string, CodingConversationPatchEvent[]>();
let eventSource: EventSource | null = null;
let connectFlight: Promise<void> | null = null;
let connectionGeneration = 0;
let store!: StoreApi<CodingConversationStoreState>;
return createStore<CodingConversationStoreState>((set, get) => ({
const bufferRecoveryPatch = (event: CodingConversationPatchEvent) => {
const buffered = recoveryPatches.get(event.conversationId) ?? [];
if (buffered.some((candidate) => (
candidate.workerGeneration === event.workerGeneration && candidate.seq === event.seq
))) return;
recoveryPatches.set(
event.conversationId,
[...buffered, event].sort((left, right) => (
left.workerGeneration - right.workerGeneration || left.seq - right.seq
)),
);
};
const replayRecoveryPatches = (conversationId: string) => {
const buffered = recoveryPatches.get(conversationId);
if (!buffered?.length) return;
recoveryPatches.delete(conversationId);
for (const event of buffered) store.getState().applyPatchEvent(event);
};
store = createStore<CodingConversationStoreState>((set, get) => ({
selectedConversationId: null,
entriesByConversationId: {},
summariesByConversationId: {},
@@ -316,6 +359,8 @@ export function createCodingConversationStore(
})
.finally(() => {
if (snapshotLoads.get(conversationId) === flight) snapshotLoads.delete(conversationId);
const entry = get().entriesByConversationId[conversationId];
if (!entry?.reducer.invalidation) replayRecoveryPatches(conversationId);
});
snapshotLoads.set(conversationId, flight);
return flight;
@@ -556,7 +601,14 @@ export function createCodingConversationStore(
if (!current.reducer.invalidation && snapshotIsOlder(current.reducer.snapshot, snapshot)) {
return state;
}
const reducer = replaceConversationSnapshot(current.reducer, snapshot);
const requests = withoutReconciledRequests(
state.requestsByConversationId[event.conversationId],
snapshot,
);
const reducer = withUnreconciledOptimisticNodes(
replaceConversationSnapshot(current.reducer, snapshot),
requests,
);
const entry: CodingConversationEntry = {
...current,
reducer,
@@ -572,17 +624,25 @@ export function createCodingConversationStore(
summariesByConversationId: summariesWithEntry(state.summariesByConversationId, entry),
requestsByConversationId: {
...state.requestsByConversationId,
[event.conversationId]: withoutReconciledRequests(
state.requestsByConversationId[event.conversationId],
snapshot,
),
[event.conversationId]: requests,
},
};
});
if (!snapshotLoads.has(event.conversationId)) {
replayRecoveryPatches(event.conversationId);
}
},
applyPatchEvent(event) {
if (event.type !== 'patch') return;
const currentBeforePatch = get().entriesByConversationId[event.conversationId];
if (currentBeforePatch?.reducer.invalidation) {
bufferRecoveryPatch(event);
if (!snapshotLoads.has(event.conversationId)) {
void get().loadSnapshot(event.conversationId, true).catch(() => undefined);
}
return;
}
let recover = false;
set((state) => {
const current = state.entriesByConversationId[event.conversationId] ?? emptyEntry();
@@ -626,6 +686,7 @@ export function createCodingConversationStore(
}
},
}));
return store;
}
export const codingConversationStore = createCodingConversationStore();