fix session routing and subagent visibility

This commit is contained in:
wuyang6
2026-06-12 11:06:04 +08:00
parent a6e7d2260b
commit 40d7c92a1d
9 changed files with 447 additions and 45 deletions
@@ -47,6 +47,7 @@ import {
readActiveSessionId,
writeActiveSessionId,
} from "@/lib/claw-active-session";
import { readSessionIdFromUrl } from "@/lib/claw-session-url";
import { cn } from "@/lib/utils";
type ActivityContextValue = {
@@ -81,7 +82,9 @@ export function ActivityProvider({ children }: { children: ReactNode }) {
}, []);
const openFiles = useCallback((nextSessionId?: string | null) => {
setMode("files");
setSessionId(nextSessionId ?? readActiveSessionId());
setSessionId(
nextSessionId ?? readSessionIdFromUrl() ?? readActiveSessionId(),
);
setFilesRefreshToken((value) => value + 1);
setOpen(true);
}, []);
@@ -141,7 +144,11 @@ type ActivityItem = {
type LiveRunEvent = NonNullable<ClawRunStatus["events"]>[number];
export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {}) {
export function ActivityPanel({
asDrawer = false,
}: {
asDrawer?: boolean;
} = {}) {
const {
open,
mode,
@@ -153,8 +160,8 @@ export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {})
close,
} = useActivityPanel();
const messages = useAuiState((s) => s.thread.messages);
const [activeSessionId, setActiveSessionId] = useState<string | null>(() =>
readActiveSessionId(),
const [activeSessionId, setActiveSessionId] = useState<string | null>(
() => readSessionIdFromUrl() ?? readActiveSessionId(),
);
const liveRunStatus = useLiveRunStatus(activeSessionId, mode);
const messageItems = useMemo(
@@ -203,18 +210,23 @@ export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {})
const prevLatestIdRef = useRef<string | null>(null);
useEffect(() => {
const update = () => setActiveSessionId(readActiveSessionId());
const update = () =>
setActiveSessionId(readSessionIdFromUrl() ?? readActiveSessionId());
const handleChanged = (event: Event) => {
const detail = (event as CustomEvent<{ sessionId?: string | null }>)
.detail;
setActiveSessionId(detail?.sessionId ?? readActiveSessionId());
setActiveSessionId(
detail?.sessionId ?? readSessionIdFromUrl() ?? readActiveSessionId(),
);
};
window.addEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.addEventListener("focus", update);
window.addEventListener("popstate", update);
update();
return () => {
window.removeEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.removeEventListener("focus", update);
window.removeEventListener("popstate", update);
};
}, []);
@@ -373,7 +385,7 @@ function SessionFilesPanel({
let cancelled = false;
async function loadFiles() {
const activeSessionId = normalizeSessionId(
sessionId ?? readActiveSessionId(),
sessionId ?? readSessionIdFromUrl() ?? readActiveSessionId(),
);
setStatus("正在读取聊天中的文件...");
try {
@@ -1019,6 +1031,86 @@ function collectLiveRunItems(runStatus: ClawRunStatus | null) {
),
});
}
items.push(...collectLiveDelegateItems(runStatus, events));
return items;
}
function collectLiveDelegateItems(
runStatus: ClawRunStatus,
events: readonly LiveRunEvent[],
): ActivityItem[] {
const items: ActivityItem[] = [];
const prefix = `live:${runStatus.run_id ?? runStatus.session_id ?? "active"}`;
for (const event of events) {
const record = event as Record<string, unknown>;
if (event.type === "delegate_group_result") {
const total = asNumber(record.subtask_count);
const completed = asNumber(record.completed_children);
const failed = asNumber(record.failed_children);
const skipped = asNumber(record.dependency_skips);
const batchCount = asNumber(record.batch_count);
items.push({
id: `${prefix}:${event.tool_call_id ?? "delegate"}:group`,
kind: "reasoning",
title: "子 Agent 汇总",
summary: [
total ? `${total} 个子任务` : "子任务完成",
`完成 ${completed}`,
failed ? `失败 ${failed}` : null,
skipped ? `跳过 ${skipped}` : null,
batchCount ? `${batchCount}` : null,
asText(record.group_status)
? `状态 ${asText(record.group_status)}`
: null,
]
.filter(Boolean)
.join(""),
status: failed ? "incomplete" : "complete",
});
}
if (event.type === "delegate_batch_result") {
const labels = Array.isArray(record.labels)
? record.labels.map((item) => String(item)).join(", ")
: "";
const failed = asNumber(record.failed_children);
items.push({
id: `${prefix}:${event.tool_call_id ?? "delegate"}:batch:${asText(record.batch_index) || items.length}`,
kind: "reasoning",
title: `子 Agent 批次 ${asText(record.batch_index) || ""}`.trim(),
summary: [
asText(record.status) || "completed",
labels,
`完成 ${asNumber(record.completed_children)}`,
failed ? `失败 ${failed}` : null,
asNumber(record.skipped_children)
? `跳过 ${asNumber(record.skipped_children)}`
: null,
]
.filter(Boolean)
.join(""),
status: failed ? "incomplete" : "complete",
});
}
if (event.type === "delegate_subtask_result") {
const failed = asText(record.stop_reason) === "backend_error";
items.push({
id: `${prefix}:${event.tool_call_id ?? "delegate"}:child:${asText(record.index) || items.length}`,
kind: "tool",
title: `子 Agent${asText(record.label) || "subtask"}`,
summary: [
asText(record.stop_reason) || "stop",
`turns=${asNumber(record.turns)}`,
`tools=${asNumber(record.tool_calls)}`,
asText(record.session_id)
? `session=${asText(record.session_id)}`
: null,
]
.filter(Boolean)
.join(""),
status: failed ? "incomplete" : "complete",
});
}
}
return items;
}
@@ -1031,21 +1123,13 @@ function mergeActivityItems(
const merged = [...messageItems];
const seen = new Set(
messageItems.map((item) =>
[
item.kind,
item.title,
item.argsText ?? "",
item.summary,
].join("\u0000"),
[item.kind, item.title, item.argsText ?? "", item.summary].join("\u0000"),
),
);
for (const item of liveItems) {
const key = [
item.kind,
item.title,
item.argsText ?? "",
item.summary,
].join("\u0000");
const key = [item.kind, item.title, item.argsText ?? "", item.summary].join(
"\u0000",
);
if (seen.has(key)) continue;
seen.add(key);
merged.push(item);
@@ -1251,6 +1335,11 @@ function summarizeTool(
part: ToolCallMessagePart,
) {
const status = getPartStatus(message, part);
if (isDelegateTool(part.toolName)) {
if (status === "running") return "子 Agent 执行中";
const delegateSummary = summarizeDelegateToolResult(part.result);
if (delegateSummary) return delegateSummary.compact;
}
if (status === "running") return "调用工具中";
if (status === "incomplete") return "工具调用未完成";
if (part.result === undefined) return "已提交工具参数";
@@ -1276,6 +1365,8 @@ function formatValue(value: unknown) {
function summarizeToolResult(value: unknown) {
const decoded = decodeJsonString(value);
if (decoded === undefined) return undefined;
const delegateSummary = summarizeDelegateToolResult(decoded);
if (delegateSummary) return delegateSummary.detail;
if (typeof decoded === "string") return trimText(decoded, 600);
if (!decoded || typeof decoded !== "object") return String(decoded);
@@ -1291,6 +1382,99 @@ function summarizeToolResult(value: unknown) {
return parts.join("\n");
}
function isDelegateTool(toolName?: string) {
return toolName === "Agent" || toolName === "delegate_agent";
}
function summarizeDelegateToolResult(value: unknown) {
const decoded = decodeJsonString(value);
if (!decoded || typeof decoded !== "object") return null;
const record = decoded as Record<string, unknown>;
const metadata =
record.metadata && typeof record.metadata === "object"
? (record.metadata as Record<string, unknown>)
: record;
if (metadata.action !== "Agent" && metadata.action !== "delegate_agent") {
return null;
}
const childResults = Array.isArray(metadata.child_results)
? metadata.child_results.filter((item): item is Record<string, unknown> =>
Boolean(item && typeof item === "object"),
)
: [];
const batches = Array.isArray(metadata.delegate_batches)
? metadata.delegate_batches.filter(
(item): item is Record<string, unknown> =>
Boolean(item && typeof item === "object"),
)
: [];
const total =
asNumber(metadata.subtask_count) ||
childResults.length ||
asNumber(metadata.completed_children) + asNumber(metadata.failed_children);
const completed = asNumber(metadata.completed_children);
const failed = asNumber(metadata.failed_children);
const skipped = asNumber(metadata.dependency_skips);
const groupStatus = asText(metadata.group_status);
const strategy = asText(metadata.strategy);
const compactParts = [
total ? `${total} 个子 Agent` : "子 Agent",
completed || failed || skipped
? `完成 ${completed} / 失败 ${failed} / 跳过 ${skipped}`
: null,
groupStatus ? `状态 ${groupStatus}` : null,
].filter(Boolean);
const compact = compactParts.join("");
const lines = [compact];
if (strategy) lines.push(`策略:${strategy}`);
if (batches.length) {
lines.push(`批次:${batches.length}`);
for (const batch of batches) {
const labels = Array.isArray(batch.labels)
? batch.labels.map((item) => String(item)).join(", ")
: "";
lines.push(
`- batch ${asText(batch.batch_index) || "?"}: ${asText(batch.status) || "unknown"}${labels ? ` · ${labels}` : ""}`,
);
}
}
if (childResults.length) {
lines.push("子任务:");
for (const child of childResults) {
const label =
asText(child.label) || `subtask_${asText(child.index) || "?"}`;
const sessionId = asText(child.session_id);
const stopReason = asText(child.stop_reason) || "stop";
const turns = asNumber(child.turns);
const toolCalls = asNumber(child.tool_calls);
const preview = asText(child.output_preview);
lines.push(
[
`- ${label}: ${stopReason}`,
turns ? `turns=${turns}` : null,
toolCalls ? `tools=${toolCalls}` : null,
sessionId ? `session=${sessionId}` : null,
]
.filter(Boolean)
.join(""),
);
if (preview) lines.push(` ${trimText(preview, 180)}`);
}
}
return { compact, detail: lines.join("\n") };
}
function asText(value: unknown) {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function asNumber(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function trimText(text: string, maxLength: number) {
const normalized = text.trim();
if (normalized.length <= maxLength) return normalized;
@@ -92,8 +92,9 @@ import {
writePendingWorkspaceSessionId,
} from "@/lib/claw-active-session";
import { useClawSessionReplay } from "@/lib/claw-session-replay";
import { cn } from "@/lib/utils";
import { readSessionIdFromUrl } from "@/lib/claw-session-url";
import { dispatchSkillToggled } from "@/lib/use-training-mode";
import { cn } from "@/lib/utils";
type ComposerInsertEvent = CustomEvent<{ text: string }>;
@@ -396,21 +397,25 @@ function usePendingWorkspaceSessionId() {
function useActiveSessionId() {
const [sessionId, setSessionId] = useState<string | null>(() =>
typeof window !== "undefined"
? normalizeSessionId(readActiveSessionId())
? normalizeSessionId(readSessionIdFromUrl() ?? readActiveSessionId())
: null,
);
useEffect(() => {
const refresh = () => {
setSessionId(normalizeSessionId(readActiveSessionId()));
setSessionId(
normalizeSessionId(readSessionIdFromUrl() ?? readActiveSessionId()),
);
};
window.addEventListener(ACTIVE_SESSION_CHANGED_EVENT, refresh);
window.addEventListener("storage", refresh);
window.addEventListener("focus", refresh);
window.addEventListener("popstate", refresh);
return () => {
window.removeEventListener(ACTIVE_SESSION_CHANGED_EVENT, refresh);
window.removeEventListener("storage", refresh);
window.removeEventListener("focus", refresh);
window.removeEventListener("popstate", refresh);
};
}, []);
@@ -1349,7 +1354,9 @@ function useComposerContextStatus() {
const statePayload = stateResponse.ok
? ((await stateResponse.json()) as ClawState)
: {};
const activeSessionId = normalizeSessionId(readActiveSessionId());
const activeSessionId = normalizeSessionId(
readSessionIdFromUrl() ?? readActiveSessionId(),
);
const latestMessageSessionId = normalizeSessionId(latestSessionId);
let sessionId = normalizeSessionId(
isRunning
@@ -2088,7 +2095,10 @@ const ActivityChainSummary: FC<{
liveStartedAtRef.current = authoritativeStartedAt;
} else if (liveStartedAtRef.current === null && storedElapsedMs !== null) {
liveStartedAtRef.current = Date.now() - storedElapsedMs;
} else if (liveStartedAtRef.current === null && messageCreatedAtMs !== null) {
} else if (
liveStartedAtRef.current === null &&
messageCreatedAtMs !== null
) {
liveStartedAtRef.current = messageCreatedAtMs;
}
const startedAt = liveStartedAtRef.current;