fix session routing and subagent visibility
This commit is contained in:
@@ -2,12 +2,17 @@ import { getCurrentAccount } from "@/lib/claw-auth";
|
||||
|
||||
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const account = await getCurrentAccount();
|
||||
if (!account) return Response.json([], { status: 401 });
|
||||
|
||||
const incoming = new URL(request.url);
|
||||
const url = new URL(`${CLAW_API_URL}/api/sessions`);
|
||||
url.searchParams.set("account_id", account.id);
|
||||
const includeChildren = incoming.searchParams.get("include_children");
|
||||
if (includeChildren) {
|
||||
url.searchParams.set("include_children", includeChildren);
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ExportedMessageRepository } from "@assistant-ui/core";
|
||||
import {
|
||||
AssistantRuntimeProvider,
|
||||
useAui,
|
||||
} from "@assistant-ui/react";
|
||||
import { AssistantRuntimeProvider, useAui } from "@assistant-ui/react";
|
||||
import {
|
||||
AssistantChatTransport,
|
||||
useChatRuntime,
|
||||
@@ -38,14 +35,14 @@ import {
|
||||
writeActiveSessionId,
|
||||
} from "@/lib/claw-active-session";
|
||||
import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
|
||||
import { pushSessionUrl } from "@/lib/claw-session-url";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { pushSessionUrl, readSessionIdFromUrl } from "@/lib/claw-session-url";
|
||||
import {
|
||||
SKILL_TOGGLED_EVENT,
|
||||
type SkillToggledDetail,
|
||||
useAppliedTraining,
|
||||
useModelIterationEnabled,
|
||||
} from "@/lib/use-training-mode";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ClawAccountGate, useClawAccount } from "./claw-account-gate";
|
||||
|
||||
type AssistantProps = {
|
||||
@@ -62,10 +59,16 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
|
||||
prepareSendMessagesRequest: async (options) => {
|
||||
const body = options.body as Record<string, unknown>;
|
||||
const messages = options.messages as UIMessage[];
|
||||
const selectedSessionId = readActiveSessionId();
|
||||
const selectedSessionId =
|
||||
readSessionIdFromUrl() ??
|
||||
readActiveSessionId() ??
|
||||
initialSessionId?.trim() ??
|
||||
null;
|
||||
const lastSessionId = getLastSessionId(messages);
|
||||
const pendingWorkspaceSessionId =
|
||||
messages.length <= 1 ? readPendingWorkspaceSessionId() : null;
|
||||
!selectedSessionId && messages.length <= 1
|
||||
? readPendingWorkspaceSessionId()
|
||||
: null;
|
||||
const selectedResumeSessionId = selectedSessionId;
|
||||
const outgoingSessionId =
|
||||
lastSessionId ??
|
||||
@@ -89,7 +92,7 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
|
||||
};
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[initialSessionId],
|
||||
);
|
||||
const runtime = useChatRuntime({
|
||||
transport,
|
||||
@@ -195,11 +198,10 @@ function getDraftKey(activeSessionId: string | null): string {
|
||||
return activeSessionId;
|
||||
}
|
||||
|
||||
|
||||
function AssistantWorkspace() {
|
||||
const aui = useAui();
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(() =>
|
||||
readActiveSessionId(),
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(
|
||||
() => readSessionIdFromUrl() ?? readActiveSessionId(),
|
||||
);
|
||||
const skill = useModelIterationEnabled();
|
||||
const { applied, setApplied } = useAppliedTraining(activeSessionId);
|
||||
@@ -250,14 +252,15 @@ function AssistantWorkspace() {
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
const next = readActiveSessionId();
|
||||
const next = readSessionIdFromUrl() ?? readActiveSessionId();
|
||||
console.log("[active-session] focus-update", { next });
|
||||
setActiveSessionId(next);
|
||||
};
|
||||
const handleChanged = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ sessionId?: string | null }>)
|
||||
.detail;
|
||||
const next = detail?.sessionId ?? readActiveSessionId();
|
||||
const next =
|
||||
detail?.sessionId ?? readSessionIdFromUrl() ?? readActiveSessionId();
|
||||
console.log("[active-session] event-changed", {
|
||||
detailSessionId: detail?.sessionId,
|
||||
next,
|
||||
@@ -266,10 +269,12 @@ function AssistantWorkspace() {
|
||||
};
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -294,8 +299,7 @@ function AssistantWorkspace() {
|
||||
"textarea.aui-composer-input",
|
||||
);
|
||||
const composerState = aui.composer().getState();
|
||||
const savingText =
|
||||
composerEl?.value ?? composerState.text ?? "";
|
||||
const savingText = composerEl?.value ?? composerState.text ?? "";
|
||||
cache.set(prevKey, savingText);
|
||||
// newTask 里点工作区会**新生成**一个 LOCALID 并切过去,用户视角里
|
||||
// 这是同一份草稿的延续——把 newTask 的文本继承到 LOCALID 名下,
|
||||
@@ -326,6 +330,7 @@ function AssistantWorkspace() {
|
||||
prevSessionKeyRef.current = nextKey;
|
||||
}, [activeSessionId, aui]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: close the activity drawer whenever training mode toggles.
|
||||
useEffect(() => {
|
||||
closeActivityPanel();
|
||||
}, [showPipeline, closeActivityPanel]);
|
||||
@@ -349,7 +354,8 @@ function AssistantWorkspace() {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
{showPipeline ? (
|
||||
<div
|
||||
<section
|
||||
aria-label="训练模式面板"
|
||||
ref={pipelineContainerRef}
|
||||
onMouseMove={handlePipelineMouseMove}
|
||||
onMouseLeave={handlePipelineMouseLeave}
|
||||
@@ -369,7 +375,7 @@ function AssistantWorkspace() {
|
||||
<span>退出训练模式</span>
|
||||
</button>
|
||||
<TrainingPipelinePanel sessionId={activeSessionId} />
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
@@ -387,7 +393,6 @@ function AssistantWorkspace() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ActivityDrawerTrigger() {
|
||||
const { open, openActivity } = useActivityPanel();
|
||||
if (open) return null;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
"use client";
|
||||
|
||||
export function readSessionIdFromUrl() {
|
||||
if (typeof window === "undefined") return null;
|
||||
const match = window.location.pathname.match(/^\/session\/([^/]+)$/);
|
||||
if (!match?.[1]) return null;
|
||||
try {
|
||||
return decodeURIComponent(match[1]).trim() || null;
|
||||
} catch {
|
||||
return match[1].trim() || null;
|
||||
}
|
||||
}
|
||||
|
||||
export function pushSessionUrl(sessionId: string) {
|
||||
if (typeof window === "undefined") return;
|
||||
const target = `/session/${encodeURIComponent(sessionId)}`;
|
||||
|
||||
Reference in New Issue
Block a user