Show session run badges
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user