Fix live activity session tracking
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
const ACTIVE_SESSION_ID_KEY = "claw.activeSessionId";
|
||||
export const ACTIVE_SESSION_CHANGED_EVENT = "claw-active-session-changed";
|
||||
|
||||
export function readActiveSessionId() {
|
||||
if (typeof window === "undefined") return null;
|
||||
@@ -19,12 +20,22 @@ export function writeActiveSessionId(sessionId: string | null | undefined) {
|
||||
}
|
||||
window.sessionStorage.setItem(ACTIVE_SESSION_ID_KEY, normalized);
|
||||
window.localStorage.setItem(ACTIVE_SESSION_ID_KEY, normalized);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ACTIVE_SESSION_CHANGED_EVENT, {
|
||||
detail: { sessionId: normalized },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function clearActiveSessionId() {
|
||||
if (typeof window === "undefined") return;
|
||||
window.sessionStorage.removeItem(ACTIVE_SESSION_ID_KEY);
|
||||
window.localStorage.removeItem(ACTIVE_SESSION_ID_KEY);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ACTIVE_SESSION_CHANGED_EVENT, {
|
||||
detail: { sessionId: null },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSessionId(value: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user