fix(ui): unread badge latches un-clearable when the read patch is never sent (#123252)

sessions.patch resolves null without sending a request when the
connection scope cannot be captured, but the read-marker guard only
unlatched on promise rejection — so the once-per-episode latch stayed
set with no request in flight, and the unread badge stayed lit on the
open session until navigating away and back. Treat a null resolution
like a failure.
This commit is contained in:
Peter Steinberger
2026-08-13 21:12:21 -07:00
committed by GitHub
parent dc61fe5abe
commit 63bc139872
2 changed files with 38 additions and 5 deletions
+14 -5
View File
@@ -189,11 +189,20 @@ export abstract class ChatPaneSession extends ChatPaneTaskSuggestions {
return;
}
const guardKey = state.sessionKey;
void this.context.sessions.patch(row.key, { unread: false }, { agentId }).catch(() => {
// Unlatch so later unread snapshots retry; the session capability
// publishes the actionable error for the owning page.
this.unreadPatchGuard.patchFailed(guardKey);
});
void this.context.sessions.patch(row.key, { unread: false }, { agentId }).then(
(result) => {
// A null result means no request was sent (connection scope lost);
// unlatch like a failure or the badge stays lit until navigation.
if (result === null) {
this.unreadPatchGuard.patchFailed(guardKey);
}
},
() => {
// Unlatch so later unread snapshots retry; the session capability
// publishes the actionable error for the owning page.
this.unreadPatchGuard.patchFailed(guardKey);
},
);
}
protected async restoreArchivedSession(sessionKey: string, expectedSessionId: string) {
@@ -90,6 +90,30 @@ describe("chat pane read markers", () => {
expect(state.lastError).toBeNull();
});
it("retries the read patch after a null (unsent) resolution", async () => {
// sessions.patch resolves null without a request when the connection
// scope is lost; the guard must unlatch like a failure or the badge
// stays lit until navigation.
const patch = vi.fn().mockResolvedValue(null);
const { pane } = createTestChatPane({
client: {} as GatewayBrowserClient,
sessions: { patch } as unknown as SessionCapability,
});
const row = {
key: "agent:main:current",
kind: "direct" as const,
label: "Unread",
updatedAt: 20,
unread: true,
};
pane.markSessionRead(row);
await Promise.resolve();
pane.markSessionRead(row);
expect(patch).toHaveBeenCalledTimes(2);
});
it("does not clear unread from a hidden retained pane", () => {
const patch = vi.fn().mockResolvedValue(null);
const { pane } = createTestChatPane({