fix(ui): hidden retained chat panes silently clear unread for sessions you are not viewing (#123247)

Since retained panes (#121625) every pane — hidden or presented — keeps
its sessions subscription alive, and applySessionsState marked the
pane's selected session read unconditionally. New activity in a
recently-viewed session was cleared by its hidden pane instantly: the
sidebar unread dot never stuck, agentStatus attention was wiped by the
read patch, and "Mark as unread" self-reverted. Gate the read-marking
on this.presented, matching applyActiveSessionBindings.
This commit is contained in:
Peter Steinberger
2026-08-13 19:37:51 -07:00
committed by GitHub
parent f391453378
commit eecf488c32
3 changed files with 46 additions and 1 deletions
+5 -1
View File
@@ -105,7 +105,11 @@ export abstract class ChatPaneContext extends ChatPaneLifecycle {
this.refreshSwarmRoster();
const selectedSession = selectedChatSessionRow(state);
if (applySelectedSessionProjection(state, selectedSession)) {
this.markSessionRead(selectedSession);
// Hidden retained panes keep this subscription alive; only the pane the
// user is actually looking at may clear unread/attention state.
if (this.presented) {
this.markSessionRead(selectedSession);
}
}
this.syncSessionSuggestionTarget(
stateValue.agentId ?? resolveChatAgentId(state) ?? "main",
@@ -89,4 +89,44 @@ describe("chat pane read markers", () => {
expect(state.chatError).toBeNull();
expect(state.lastError).toBeNull();
});
it("does not clear unread from a hidden retained pane", () => {
const patch = vi.fn().mockResolvedValue(null);
const { pane } = createTestChatPane({
client: {} as GatewayBrowserClient,
sessions: { patch } as unknown as SessionCapability,
});
const sessionsState = (presented: boolean) => {
pane.presented = presented;
pane.applySessionsState({
result: {
sessions: [
{
key: "agent:main:current",
kind: "direct",
label: "Background activity",
updatedAt: 20,
unread: true,
},
],
},
agentId: "main",
loading: false,
error: null,
deletedSessions: [],
} as unknown as Parameters<typeof pane.applySessionsState>[0]);
};
// Hidden retained panes keep the subscription alive but must not mark
// the session read — the user is not looking at it.
sessionsState(false);
expect(patch).not.toHaveBeenCalled();
sessionsState(true);
expect(patch).toHaveBeenCalledWith(
"agent:main:current",
{ unread: false },
{ agentId: "main" },
);
});
});
@@ -134,6 +134,7 @@ export type TestChatPane = HTMLElement & {
headerPlacementReclaimingKey: string | null;
reclaimHeaderPlacement: (row: GatewaySessionRow) => Promise<void>;
markSessionRead: (row: GatewaySessionRow | undefined) => void;
applySessionsState: (stateValue: ApplicationContext["sessions"]["state"]) => void;
renderPaneHeader: (
workspace: ReturnType<typeof createSessionWorkspaceProps>,
tasks: ReturnType<typeof createBackgroundTasksProps>,