fix session routing and subagent visibility
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user