fix(editor): stop setState-during-render loop in multi-message annotate (#949) (#950)

buildMessageAnnotationEntries() is reachable on the render path via
currentFeedbackPayload (useMemo) -> getCurrentFeedbackPayload ->
buildFullAnnotationsOutput. It called saveCurrentMessageState(), which
writes React state (setCachedMessageAnnotationCounts) with a fresh Map
each call, so React's Object.is bail-out never fires. In multi-message
mode (annotateSource === 'message' && recentMessages.length > 1) this is
an infinite re-render — React throws #301 and the page renders blank.
This broke /plannotator-last on Pi for any conversation with 2+ assistant
messages.

Switch the render-path read to getMessageStatesWithCurrent(), which
returns the same merged states (cached + live current message) without
the setState side effect. Cache persistence still happens in event
handlers (handleSelectMessage). Exported feedback is unchanged.

Root cause introduced in 1fa60522. Reported and diagnosed by @emmaneugene.

Closes #949
This commit is contained in:
Michael Ramos
2026-06-22 05:24:05 -07:00
committed by GitHub
parent 90bc8f8db5
commit f88fd8ab0e
+9 -2
View File
@@ -896,7 +896,14 @@ const App: React.FC = () => {
const buildMessageAnnotationEntries = React.useCallback((): MessageAnnotationEntry[] => {
if (annotateSource !== 'message' || recentMessages.length === 0) return [];
const states = saveCurrentMessageState();
// Must be a PURE read: this runs on the render path via
// currentFeedbackPayload (useMemo) -> getCurrentFeedbackPayload ->
// buildFullAnnotationsOutput. saveCurrentMessageState() writes React state
// (setCachedMessageAnnotationCounts), which during render is an infinite
// re-render loop in multi-message mode (#949). getMessageStatesWithCurrent
// returns the same merged data without the setState side effect; the cache
// persistence happens in event handlers (handleSelectMessage) instead.
const states = getMessageStatesWithCurrent();
return recentMessages.map((msg) => {
const state = states.get(msg.messageId) ?? createEmptyMessageState(msg);
const linkedDocs: Map<string, LinkedDocAnnotationEntry> = new Map();
@@ -917,7 +924,7 @@ const App: React.FC = () => {
codeAnnotations: state.codeAnnotations,
};
});
}, [annotateSource, recentMessages, saveCurrentMessageState]);
}, [annotateSource, recentMessages, getMessageStatesWithCurrent]);
const activeMessageAnnotationCounts = React.useMemo(() => {
const counts = new Map(cachedMessageAnnotationCounts);