Fix live activity session tracking

This commit is contained in:
wuyang6
2026-05-12 20:22:50 +08:00
parent 64787498dd
commit c118e010e3
2 changed files with 230 additions and 1 deletions
@@ -31,6 +31,10 @@ import {
useRef,
useState,
} from "react";
import {
type ClawRunStatus,
fetchLatestRunStatus,
} from "@/components/assistant-ui/thread-list";
import { Button } from "@/components/ui/button";
import {
Collapsible,
@@ -38,6 +42,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
ACTIVE_SESSION_CHANGED_EVENT,
readActiveSessionId,
writeActiveSessionId,
} from "@/lib/claw-active-session";
@@ -133,6 +138,8 @@ type ActivityItem = {
fileLinks?: string[];
};
type LiveRunEvent = NonNullable<ClawRunStatus["events"]>[number];
export function ActivityPanel() {
const {
open,
@@ -145,13 +152,49 @@ export function ActivityPanel() {
close,
} = useActivityPanel();
const messages = useAuiState((s) => s.thread.messages);
const items = useMemo(() => collectActivityItems(messages), [messages]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(() =>
readActiveSessionId(),
);
const liveRunStatus = useLiveRunStatus(activeSessionId, mode);
const replayedSessionId = useMemo(
() => findLatestSessionIdInMessages(messages),
[messages],
);
const messageItems = useMemo(
() => collectActivityItems(messages),
[messages],
);
const liveItems = useMemo(
() => collectLiveRunItems(liveRunStatus),
[liveRunStatus],
);
const shouldUseLiveItems =
liveItems.length > 0 &&
activeSessionId !== null &&
(activeSessionId !== replayedSessionId || hasActiveRun(liveRunStatus));
const items = shouldUseLiveItems ? liveItems : messageItems;
const selectedMessageId = selectedId ? activityMessageId(selectedId) : null;
const visibleItems = selectedMessageId
? items.filter((item) => activityMessageId(item.id) === selectedMessageId)
: items;
const prevLatestIdRef = useRef<string | null>(null);
useEffect(() => {
const update = () => setActiveSessionId(readActiveSessionId());
const handleChanged = (event: Event) => {
const detail = (event as CustomEvent<{ sessionId?: string | null }>)
.detail;
setActiveSessionId(detail?.sessionId ?? readActiveSessionId());
};
window.addEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.addEventListener("focus", update);
update();
return () => {
window.removeEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.removeEventListener("focus", update);
};
}, []);
useEffect(() => {
const latestId = items.at(-1)?.id ?? null;
if (
@@ -657,6 +700,60 @@ function activityMessageId(activityId: string) {
return activityId.split(":", 1)[0] ?? activityId;
}
function useLiveRunStatus(
sessionId: string | null,
mode: ActivityContextValue["mode"],
) {
const [status, setStatus] = useState<ClawRunStatus | null>(null);
useEffect(() => {
if (!sessionId || mode !== "activity") {
setStatus(null);
return;
}
const safeSessionId = sessionId;
let cancelled = false;
async function refresh() {
const nextStatus = await fetchLatestRunStatus(safeSessionId);
if (cancelled) return;
setStatus(hasActiveRun(nextStatus) ? nextStatus : null);
}
refresh();
const interval = window.setInterval(refresh, 1000);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [sessionId, mode]);
return status;
}
function hasActiveRun(
status?: ClawRunStatus | null,
): status is ClawRunStatus & { status: "queued" | "running" } {
return status?.status === "queued" || status?.status === "running";
}
function findLatestSessionIdInMessages(messages: readonly MessageState[]) {
for (const message of [...messages].reverse()) {
const metadata = message.metadata;
const custom =
metadata && typeof metadata === "object" && "custom" in metadata
? (metadata as { custom?: unknown }).custom
: null;
if (custom && typeof custom === "object" && "sessionId" in custom) {
const sessionId = (custom as { sessionId?: unknown }).sessionId;
if (typeof sessionId === "string" && sessionId.trim()) {
return sessionId.trim();
}
}
}
return null;
}
function ActivityPanelItem({
item,
open,
@@ -770,6 +867,118 @@ function ActivityResultSummary({ value }: { value: string }) {
);
}
function collectLiveRunItems(runStatus: ClawRunStatus | null) {
if (!hasActiveRun(runStatus)) return [];
const items: ActivityItem[] = [];
const events = runStatus.events ?? [];
const summaryLines = summarizeLiveRunEvents(runStatus, events);
items.push({
id: `live:${runStatus.run_id ?? runStatus.session_id ?? "active"}:reasoning`,
kind: "reasoning",
title: runStatus.status === "queued" ? "排队中" : "思考中",
summary: summaryLines.join("\n"),
status: "running",
});
const resultById = new Map<string, LiveRunEvent>();
for (const event of events) {
if (event.type === "tool_result" && event.tool_call_id) {
resultById.set(event.tool_call_id, event);
}
}
const seenToolCalls = new Set<string>();
for (const event of events) {
if (
event.type !== "tool_start" ||
!event.tool_call_id ||
!event.tool_name ||
seenToolCalls.has(event.tool_call_id)
) {
continue;
}
seenToolCalls.add(event.tool_call_id);
const result = resultById.get(event.tool_call_id);
const resultSummary =
typeof result?.metadata?.output_preview === "string"
? result.metadata.output_preview
: result
? result.ok === false
? "工具执行失败,等待最终结果汇总。"
: "工具调用完成,等待最终结果汇总。"
: undefined;
const input = stripClawToolMetadata(
attachStageNote(event.arguments ?? {}, event.assistant_content),
);
const argsText = formatToolArgs(input);
items.push({
id: `live:${runStatus.run_id ?? runStatus.session_id ?? "active"}:${event.tool_call_id}`,
kind: "tool",
title: event.tool_name,
summary: result
? result.ok === false
? "工具调用失败"
: "工具调用完成"
: "工具调用中",
status: result
? result.ok === false
? "incomplete"
: "complete"
: "running",
argsText,
resultSummary,
rawResult: resultSummary,
fileLinks: extractLocalPaths(
[argsText, resultSummary].filter(Boolean) as string[],
),
});
}
return items;
}
function summarizeLiveRunEvents(
runStatus: ClawRunStatus,
events: readonly LiveRunEvent[],
) {
const lines: string[] = [];
for (const event of events) {
if (event.type === "run_queued") {
lines.push("当前会话已有任务在执行,本轮正在排队。");
}
if (event.type === "run_started") {
lines.push("后端已开始执行本轮任务。");
}
if (event.type === "tool_start") {
const stageNote =
typeof event.assistant_content === "string"
? event.assistant_content.trim()
: "";
lines.push(
stageNote ||
(event.tool_name ? `调用工具 ${event.tool_name}` : "正在调用工具"),
);
}
if (event.type === "tool_result") {
lines.push(
event.tool_name ? `工具完成 ${event.tool_name}` : "工具调用完成",
);
}
if (event.type === "final_text_start") {
lines.push("正在整理回复");
}
}
if (runStatus.current_stage && lines.at(-1) !== runStatus.current_stage) {
lines.push(runStatus.current_stage);
}
if (!lines.length) {
lines.push(
runStatus.status === "queued"
? "当前会话已有任务在执行,本轮正在排队。"
: "后端正在执行这个会话。",
);
}
return lines.slice(-40);
}
function collectActivityItems(messages: readonly MessageState[]) {
const items: ActivityItem[] = [];
@@ -841,6 +1050,15 @@ function getStageNote(value: unknown) {
return typeof note === "string" ? note.trim() : "";
}
function attachStageNote(input: unknown, stageNote?: unknown) {
const note = typeof stageNote === "string" ? stageNote.trim() : "";
if (!note) return input;
if (input && typeof input === "object" && !Array.isArray(input)) {
return { ...(input as Record<string, unknown>), __claw_stage_note: note };
}
return { value: input, __claw_stage_note: note };
}
function stripClawToolMetadata(value: unknown): unknown {
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
const {