Stabilize chat read-state effect against snapshot reloads

useSnapshot now memoizes the actions object so dependants don't see a
fresh reference on every render. ChatPage's mark-read effect keys off
activeSpaceId/latestActiveMessageId primitives instead of the snapshot
objects that re-allocate on reload, which prevents the markRead → reload
→ rerender loop that was deadlocking the Electron click path under
Playwright.

Also constrains HomePage agenda time to same-day activity so the
'Today' column doesn't surface a meaningless wall-clock for older tasks.
This commit is contained in:
ZacharyZhang-NY committed 2026-05-21 20:32:25 -04:00
1 parent 1f78ae6f2e
commit 0eb1810246
3 files changed
+21 -9

No files matched your search

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { AppSnapshot } from "../../../shared/domain";
import type { DesktopResult } from "../../../shared/result";
@@ -35,5 +35,6 @@ export function useSnapshot(): {
});
}, [load]);
return { snapshot, actions: { run, reload: load } };
const actions = useMemo<SnapshotActions>(() => ({ run, reload: load }), [run, load]);
return { snapshot, actions };
}
@@ -47,20 +47,20 @@ export function ChatPage({ snapshot, actions }: PageProps) {
: fallbackSpaceId;
const activeSpace = spaces.find((space) => space.id === activeSpaceId) ?? null;
const latestActiveMessageId = useMemo(() => {
if (!activeSpace) return null;
const inSpace = messagesAll.filter((message) => message.spaceId === activeSpace.id);
if (!activeSpaceId) return null;
const inSpace = messagesAll.filter((message) => message.spaceId === activeSpaceId);
if (inSpace.length === 0) return null;
return inSpace[inSpace.length - 1].id;
}, [messagesAll, activeSpace]);
}, [messagesAll, activeSpaceId]);
useEffect(() => {
if (!activeSpace || !latestActiveMessageId) return;
if (!activeSpaceId || !latestActiveMessageId) return;
void actions.run(() => window.agentCompany.markChatSpaceRead({
orgId,
employeeId: employee.id,
spaceId: activeSpace.id,
spaceId: activeSpaceId,
}));
}, [activeSpace, latestActiveMessageId, orgId, employee.id, actions]);
}, [activeSpaceId, latestActiveMessageId, orgId, employee.id, actions]);
const messagesInSpace = useMemo(
() => (activeSpace ? messagesAll.filter((m) => m.spaceId === activeSpace.id) : []),
@@ -102,7 +102,7 @@ function TodayCard({
{rows.map((task) => {
const owner = agents.find((agent) => agent.id === task.assigneeAgentId);
const ownerName = owner?.name ?? employee.displayName;
const time = formatTime(task.updatedAt);
const time = agendaTime(task, date);
const requiresApproval = approvals.some((approval) => approval.title === task.title);
return (
<div key={task.id} className="row" style={{ gap: 12 }}>
@@ -279,6 +279,17 @@ function GoalCard({
);
}
function agendaTime(task: TaskRecord, reference: Date): string {
const updated = new Date(task.updatedAt);
if (Number.isNaN(updated.getTime())) return "—";
const sameDay =
updated.getFullYear() === reference.getFullYear() &&
updated.getMonth() === reference.getMonth() &&
updated.getDate() === reference.getDate();
if (!sameDay) return "—";
return formatTime(task.updatedAt);
}
function collectAgendaTasks(tasks: TaskRecord[], reference: Date): TaskRecord[] {
const startOfDay = new Date(reference.getFullYear(), reference.getMonth(), reference.getDate()).getTime();
const endOfDay = startOfDay + 24 * 60 * 60 * 1000;