Skip to content

Commit 93f39ee

Browse files
IM.codesclaude
andcommitted
fix(web): queued message UI flashes away before drain dispatches
transport-relay's onComplete emits session.state idle WITHOUT pending message fields. The frontend was unconditionally clearing the queue on idle, causing queued messages to visually disappear before the drain event dispatches them (~100ms later). Fix: only clear pending on idle when the event carries the pendingMessages field (hasPendingMessagesField=true). transport-relay's bare idle keeps existing pending; the subsequent runtime idle (with pending=[]) or drain running event clears them authoritatively. Applied to both main sessions (app.tsx) and sub-sessions (useSubSessions.ts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cd2b58a commit 93f39ee

2 files changed

Lines changed: 23 additions & 3 deletions

File tree

web/src/app.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1421,7 +1421,17 @@ export function App() {
14211421
} else if (liveState === 'idle') {
14221422
setSessions((prev) => prev.map((s) =>
14231423
s.name === event.sessionId
1424-
? { ...s, state: liveState as SessionInfo['state'], transportPendingMessages: [], transportPendingMessageEntries: [] }
1424+
? {
1425+
...s,
1426+
state: liveState as SessionInfo['state'],
1427+
// Only clear pending when the event carries authoritative pending data.
1428+
// transport-relay's onComplete emits idle WITHOUT pending fields — clearing
1429+
// here would flash-remove queued messages before drain dispatches them.
1430+
// The subsequent runtime idle (with pending=[]) or drain running event will clear.
1431+
...(hasPendingMessagesField
1432+
? { transportPendingMessages: extractTransportPendingMessages(event.payload.pendingMessages), transportPendingMessageEntries: normalizeTransportPendingEntries(event.payload.pendingMessageEntries, extractTransportPendingMessages(event.payload.pendingMessages), event.sessionId) }
1433+
: {}),
1434+
}
14251435
: s,
14261436
));
14271437
}

web/src/hooks/useSubSessions.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,19 @@ export function useSubSessions(
318318
next[idx] = { ...next[idx], state: 'running' };
319319
return next;
320320
}
321-
if (prev[idx].state === state && (prev[idx].transportPendingMessages?.length ?? 0) === 0 && (prev[idx].transportPendingMessageEntries?.length ?? 0) === 0) return prev;
321+
// For idle: only clear pending when the event carries authoritative pending data.
322+
// transport-relay's onComplete emits idle WITHOUT pending fields — clearing here
323+
// would flash-remove queued messages before drain dispatches them.
324+
const shouldClearPending = state !== 'idle' || hasPendingMessagesField;
325+
if (prev[idx].state === state && !shouldClearPending && (prev[idx].transportPendingMessages?.length ?? 0) === 0 && (prev[idx].transportPendingMessageEntries?.length ?? 0) === 0) return prev;
322326
const next = [...prev];
323-
next[idx] = { ...next[idx], state: state as SubSession['state'], transportPendingMessages: [], transportPendingMessageEntries: [] };
327+
next[idx] = {
328+
...next[idx],
329+
state: state as SubSession['state'],
330+
...(shouldClearPending
331+
? { transportPendingMessages: [], transportPendingMessageEntries: [] }
332+
: {}),
333+
};
324334
return next;
325335
});
326336
});

0 commit comments

Comments
 (0)