Keep optimistic user message during replay

This commit is contained in:
wuyang6
2026-06-12 20:07:42 +08:00
parent 611284b27a
commit c3caa4a1e3
2 changed files with 74 additions and 8 deletions
+59 -1
View File
@@ -8,7 +8,14 @@ import {
} from "@assistant-ui/react-ai-sdk"; } from "@assistant-ui/react-ai-sdk";
import type { UIMessage } from "ai"; import type { UIMessage } from "ai";
import { PanelRightOpenIcon, XIcon } from "lucide-react"; import { PanelRightOpenIcon, XIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import {
type MutableRefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { import {
ActivityPanel, ActivityPanel,
ActivityProvider, ActivityProvider,
@@ -38,6 +45,7 @@ import {
import { import {
fetchSessionReplaySnapshot, fetchSessionReplaySnapshot,
readCachedSessionReplay, readCachedSessionReplay,
type SessionReplaySnapshot,
} from "@/lib/claw-session-cache"; } from "@/lib/claw-session-cache";
import { ClawSessionReplayProvider } from "@/lib/claw-session-replay"; import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
import { pushSessionUrl, readSessionIdFromUrl } from "@/lib/claw-session-url"; import { pushSessionUrl, readSessionIdFromUrl } from "@/lib/claw-session-url";
@@ -54,9 +62,12 @@ type AssistantProps = {
initialSessionId?: string; initialSessionId?: string;
}; };
const OPTIMISTIC_REPLAY_GUARD_MS = 30_000;
export const Assistant = ({ initialSessionId }: AssistantProps = {}) => { export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
const { account, isLoading, setAccount } = useClawAccount(); const { account, isLoading, setAccount } = useClawAccount();
const loadedInitialSessionRef = useRef<string | null>(null); const loadedInitialSessionRef = useRef<string | null>(null);
const optimisticSendRef = useRef<Map<string, number>>(new Map());
const transport = useMemo( const transport = useMemo(
() => () =>
new AssistantChatTransport({ new AssistantChatTransport({
@@ -78,6 +89,9 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
const lastUserMessage = [...messages] const lastUserMessage = [...messages]
.reverse() .reverse()
.find((message) => message.role === "user"); .find((message) => message.role === "user");
if (lastUserMessage) {
optimisticSendRef.current.set(outgoingSessionId, Date.now());
}
writeActiveSessionId(outgoingSessionId); writeActiveSessionId(outgoingSessionId);
pushSessionUrl(outgoingSessionId); pushSessionUrl(outgoingSessionId);
@@ -126,6 +140,13 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
Awaited<ReturnType<typeof fetchLatestRunStatus>> Awaited<ReturnType<typeof fetchLatestRunStatus>>
>(targetSessionId); >(targetSessionId);
if (cached && !cancelled) { if (cached && !cancelled) {
if (
!shouldSkipOptimisticEmptyReplay(
optimisticSendRef,
targetSessionId,
cached,
)
) {
replaySession( replaySession(
targetSessionId, targetSessionId,
toReplayRepository( toReplayRepository(
@@ -135,12 +156,22 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
), ),
); );
} }
}
const snapshot = await fetchSessionReplaySnapshot< const snapshot = await fetchSessionReplaySnapshot<
Parameters<typeof toReplayRepository>[0], Parameters<typeof toReplayRepository>[0],
Awaited<ReturnType<typeof fetchLatestRunStatus>> Awaited<ReturnType<typeof fetchLatestRunStatus>>
>(targetSessionId, { signal: abortController.signal }); >(targetSessionId, { signal: abortController.signal });
if (cancelled) return; if (cancelled) return;
if (!snapshot) return; if (!snapshot) return;
if (
shouldSkipOptimisticEmptyReplay(
optimisticSendRef,
targetSessionId,
snapshot,
)
) {
return;
}
if (!cached || snapshot.signature !== cached.signature) { if (!cached || snapshot.signature !== cached.signature) {
replaySession( replaySession(
targetSessionId, targetSessionId,
@@ -203,6 +234,33 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
); );
}; };
function shouldSkipOptimisticEmptyReplay(
optimisticSendRef: MutableRefObject<Map<string, number>>,
sessionId: string,
snapshot: SessionReplaySnapshot | null,
) {
if (hasReplayMessages(snapshot)) {
optimisticSendRef.current.delete(sessionId);
return false;
}
const sentAt = optimisticSendRef.current.get(sessionId);
if (sentAt === undefined) return false;
if (Date.now() - sentAt > OPTIMISTIC_REPLAY_GUARD_MS) {
optimisticSendRef.current.delete(sessionId);
return false;
}
return true;
}
function hasReplayMessages(snapshot: SessionReplaySnapshot | null) {
const session = snapshot?.session;
if (!session || typeof session !== "object" || Array.isArray(session)) {
return false;
}
const messages = (session as { messages?: unknown }).messages;
return Array.isArray(messages) && messages.length > 0;
}
// 共享 composer 草稿缓存里 newTask(无 sessionId)使用的 key。 // 共享 composer 草稿缓存里 newTask(无 sessionId)使用的 key。
const NEWTASK_KEY = "__newtask__"; const NEWTASK_KEY = "__newtask__";
+8
View File
@@ -74,6 +74,9 @@ async function fetchSessionStateSnapshot<TSession, TRun>(
.sort((left, right) => (left.seq ?? 0) - (right.seq ?? 0)) .sort((left, right) => (left.seq ?? 0) - (right.seq ?? 0))
.map((entry) => entry.message) .map((entry) => entry.message)
.filter((message) => Boolean(message)); .filter((message) => Boolean(message));
if (orderedMessages.length === 0 && !isActiveRunLike(payload.run)) {
return null;
}
const session = { const session = {
...(payload.session ?? {}), ...(payload.session ?? {}),
session_id: sessionId, session_id: sessionId,
@@ -157,6 +160,11 @@ function sessionReplaySignature(session: unknown, runStatusValue: unknown) {
].join("|"); ].join("|");
} }
function isActiveRunLike(value: unknown) {
if (!isObject(value)) return false;
return value.status === "queued" || value.status === "running";
}
function isObject(value: unknown): value is Record<string, unknown> { function isObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value); return Boolean(value) && typeof value === "object" && !Array.isArray(value);
} }