Show session run badges

This commit is contained in:
wuyang6
2026-06-12 18:28:00 +08:00
parent 4ba8761db2
commit 5bcc22622e
@@ -129,6 +129,9 @@ type ClawReplaySnapshot = SessionReplaySnapshot<
ClawRunStatus ClawRunStatus
>; >;
const SESSION_BADGE_POLL_INTERVAL_MS = 2000;
const MAX_SESSION_BADGE_POLL_COUNT = 80;
function replaySessionSnapshot( function replaySessionSnapshot(
replaySession: ReplaySessionFn, replaySession: ReplaySessionFn,
sessionId: string, sessionId: string,
@@ -359,6 +362,7 @@ const ClawSessionList: FC = () => {
const renameInputRef = useRef<HTMLInputElement | null>(null); const renameInputRef = useRef<HTMLInputElement | null>(null);
const openRequestRef = useRef(0); const openRequestRef = useRef(0);
const openAbortRef = useRef<AbortController | null>(null); const openAbortRef = useRef<AbortController | null>(null);
const sessionBadges = useSessionRunBadges(sessions, activeSessionId);
useEffect(() => { useEffect(() => {
setActiveSessionId(readActiveSessionId()); setActiveSessionId(readActiveSessionId());
@@ -423,6 +427,7 @@ const ClawSessionList: FC = () => {
}; };
const openSession = async (sessionId: string) => { const openSession = async (sessionId: string) => {
sessionBadges.markRead(sessionId);
const requestId = openRequestRef.current + 1; const requestId = openRequestRef.current + 1;
openRequestRef.current = requestId; openRequestRef.current = requestId;
openAbortRef.current?.abort(); openAbortRef.current?.abort();
@@ -473,6 +478,7 @@ const ClawSessionList: FC = () => {
{sessions.map((session) => { {sessions.map((session) => {
const active = activeSessionId === session.session_id; const active = activeSessionId === session.session_id;
const loading = loadingSessionId === session.session_id; const loading = loadingSessionId === session.session_id;
const badge = sessionBadges.bySessionId.get(session.session_id);
return ( return (
<div <div
key={`claw-session-${session.session_id}-${session.modified_at}`} key={`claw-session-${session.session_id}-${session.modified_at}`}
@@ -526,6 +532,10 @@ const ClawSessionList: FC = () => {
</span> </span>
</button> </button>
)} )}
<SessionRunBadge
running={badge?.running === true}
unread={badge?.unread === true}
/>
<button <button
type="button" type="button"
className="aui-thread-list-item-more mr-2 flex size-7 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100 data-[busy=true]:opacity-100 data-[open=true]:bg-accent data-[open=true]:opacity-100" className="aui-thread-list-item-more mr-2 flex size-7 shrink-0 items-center justify-center rounded-md p-0 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100 data-[busy=true]:opacity-100 data-[open=true]:bg-accent data-[open=true]:opacity-100"
@@ -601,6 +611,137 @@ const ClawSessionList: FC = () => {
); );
}; };
function useSessionRunBadges(
sessions: ClawSession[],
activeSessionId: string | null,
) {
const [runningIds, setRunningIds] = useState<Set<string>>(() => new Set());
const [unreadIds, setUnreadIds] = useState<Set<string>>(() => new Set());
const previousRunningIdsRef = useRef<Set<string>>(new Set());
useEffect(() => {
if (!activeSessionId) return;
setUnreadIds((current) => {
if (!current.has(activeSessionId)) return current;
const next = new Set(current);
next.delete(activeSessionId);
return next;
});
}, [activeSessionId]);
useEffect(() => {
const polledSessionIds = sessions
.slice(0, MAX_SESSION_BADGE_POLL_COUNT)
.map((session) => session.session_id);
const knownSessionIds = new Set(polledSessionIds);
if (polledSessionIds.length === 0) {
setRunningIds(new Set());
setUnreadIds(new Set());
previousRunningIdsRef.current = new Set();
return;
}
let cancelled = false;
async function refreshBadges() {
const results = await Promise.allSettled(
polledSessionIds.map(async (sessionId) => ({
sessionId,
status: await fetchLatestRunStatus(sessionId),
})),
);
if (cancelled) return;
const nextRunningIds = new Set<string>();
const completedSessionIds: string[] = [];
for (const result of results) {
if (result.status !== "fulfilled") continue;
const { sessionId, status } = result.value;
if (isActiveRunStatus(status)) {
nextRunningIds.add(sessionId);
} else if (previousRunningIdsRef.current.has(sessionId)) {
completedSessionIds.push(sessionId);
}
}
setRunningIds(nextRunningIds);
setUnreadIds((current) => {
const next = new Set(
[...current].filter((sessionId) => knownSessionIds.has(sessionId)),
);
for (const sessionId of completedSessionIds) {
if (sessionId !== activeSessionId) {
next.add(sessionId);
}
}
if (activeSessionId) next.delete(activeSessionId);
return next;
});
previousRunningIdsRef.current = nextRunningIds;
}
void refreshBadges();
const interval = window.setInterval(
refreshBadges,
SESSION_BADGE_POLL_INTERVAL_MS,
);
window.addEventListener("claw-sessions-changed", refreshBadges);
return () => {
cancelled = true;
window.clearInterval(interval);
window.removeEventListener("claw-sessions-changed", refreshBadges);
};
}, [sessions, activeSessionId]);
const bySessionId = new Map<string, { running: boolean; unread: boolean }>();
for (const session of sessions) {
bySessionId.set(session.session_id, {
running: runningIds.has(session.session_id),
unread: unreadIds.has(session.session_id),
});
}
const markRead = (sessionId: string) => {
setUnreadIds((current) => {
if (!current.has(sessionId)) return current;
const next = new Set(current);
next.delete(sessionId);
return next;
});
};
return { bySessionId, markRead };
}
function SessionRunBadge({
running,
unread,
}: {
running: boolean;
unread: boolean;
}) {
if (running) {
return (
<span
className="flex size-5 shrink-0 items-center justify-center text-muted-foreground"
title="运行中"
>
<Loader2Icon className="size-3.5 animate-spin" />
</span>
);
}
if (unread) {
return (
<span
className="flex size-5 shrink-0 items-center justify-center"
title="已完成,未查看"
>
<span className="size-2 rounded-full bg-sky-300" />
</span>
);
}
return <span aria-hidden="true" className="size-5 shrink-0" />;
}
function dedupeSessions(payload: unknown[]) { function dedupeSessions(payload: unknown[]) {
const byId = new Map<string, ClawSession>(); const byId = new Map<string, ClawSession>();
for (const item of payload) { for (const item of payload) {