Show session run badges
This commit is contained in:
@@ -129,6 +129,9 @@ type ClawReplaySnapshot = SessionReplaySnapshot<
|
||||
ClawRunStatus
|
||||
>;
|
||||
|
||||
const SESSION_BADGE_POLL_INTERVAL_MS = 2000;
|
||||
const MAX_SESSION_BADGE_POLL_COUNT = 80;
|
||||
|
||||
function replaySessionSnapshot(
|
||||
replaySession: ReplaySessionFn,
|
||||
sessionId: string,
|
||||
@@ -359,6 +362,7 @@ const ClawSessionList: FC = () => {
|
||||
const renameInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const openRequestRef = useRef(0);
|
||||
const openAbortRef = useRef<AbortController | null>(null);
|
||||
const sessionBadges = useSessionRunBadges(sessions, activeSessionId);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSessionId(readActiveSessionId());
|
||||
@@ -423,6 +427,7 @@ const ClawSessionList: FC = () => {
|
||||
};
|
||||
|
||||
const openSession = async (sessionId: string) => {
|
||||
sessionBadges.markRead(sessionId);
|
||||
const requestId = openRequestRef.current + 1;
|
||||
openRequestRef.current = requestId;
|
||||
openAbortRef.current?.abort();
|
||||
@@ -473,6 +478,7 @@ const ClawSessionList: FC = () => {
|
||||
{sessions.map((session) => {
|
||||
const active = activeSessionId === session.session_id;
|
||||
const loading = loadingSessionId === session.session_id;
|
||||
const badge = sessionBadges.bySessionId.get(session.session_id);
|
||||
return (
|
||||
<div
|
||||
key={`claw-session-${session.session_id}-${session.modified_at}`}
|
||||
@@ -526,6 +532,10 @@ const ClawSessionList: FC = () => {
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<SessionRunBadge
|
||||
running={badge?.running === true}
|
||||
unread={badge?.unread === true}
|
||||
/>
|
||||
<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"
|
||||
@@ -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[]) {
|
||||
const byId = new Map<string, ClawSession>();
|
||||
for (const item of payload) {
|
||||
|
||||
Reference in New Issue
Block a user