From 4de04234d0eb98ef8b70ae1f2c219b060449e289 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 24 Jun 2026 21:44:22 +0800 Subject: [PATCH] fix: persist Claw composer clear guard --- components/canvas/CanvasChatPanel.tsx | 191 +++++++++++++++++++++++--- 1 file changed, 172 insertions(+), 19 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 09feca94c..8e8c62365 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -333,6 +333,9 @@ const CLAWD_TABLE_MAX_FILES = 3; const CLAWD_TABLE_MAX_BYTES = 10 * 1024 * 1024; const CLAWD_PROCESSING_HINT = "已收到,这个任务稍微繁重一些,处理时间可能会长一点,请耐心等候。"; +const CLAWD_SUBMITTED_COMPOSER_RECOVERY_TTL_MS = 30 * 60 * 1000; +const CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY = + "ai-chatbot:clawd-submitted-composer"; const CLAWD_SIMPLE_PROMPT_PATTERNS = [ /^(hi|hello|hey|你好|您好|哈喽|嗨|在吗|早上好|下午好|晚上好)[\s!.。!??~~]*$/i, /^(你是谁|你叫什么|你叫什么名字|你能做什么|你可以做什么|有什么功能|介绍一下自己|帮我介绍一下你)[\s!.。!??~~]*$/, @@ -359,6 +362,85 @@ function normalizeClawdComposerText(value: string) { .trim(); } +type ClawdComposerSubmissionSnapshot = { + chatId: string; + expiresAt: number; + startedAt: number; + submittedTexts: string[]; +}; + +function isClawdSubmittedComposerText( + snapshot: ClawdComposerSubmissionSnapshot, + value: string +) { + const normalizedValue = normalizeClawdComposerText(value); + if (!normalizedValue) return false; + + return snapshot.submittedTexts.some( + (text) => normalizeClawdComposerText(text) === normalizedValue + ); +} + +function parseClawdComposerSubmissionSnapshot( + value: unknown +): ClawdComposerSubmissionSnapshot | null { + if (!value || typeof value !== "object") return null; + const snapshot = value as Partial; + const chatId = String(snapshot.chatId || "").trim(); + const startedAt = Number(snapshot.startedAt); + const expiresAt = Number(snapshot.expiresAt); + const submittedTexts = Array.isArray(snapshot.submittedTexts) + ? snapshot.submittedTexts + .map((text) => normalizeClawdComposerText(String(text || ""))) + .filter(Boolean) + : []; + + if (!chatId || !Number.isFinite(startedAt) || !Number.isFinite(expiresAt)) { + return null; + } + if (submittedTexts.length === 0) { + return null; + } + + return { + chatId, + expiresAt, + startedAt, + submittedTexts, + }; +} + +function readStoredClawdComposerSubmissionSnapshot() { + if (typeof window === "undefined") return null; + try { + const raw = window.sessionStorage.getItem( + CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY + ); + if (!raw) return null; + return parseClawdComposerSubmissionSnapshot(JSON.parse(raw)); + } catch { + return null; + } +} + +function writeStoredClawdComposerSubmissionSnapshot( + snapshot: ClawdComposerSubmissionSnapshot | null +) { + if (typeof window === "undefined") return; + try { + if (!snapshot) { + window.sessionStorage.removeItem(CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY); + return; + } + window.sessionStorage.setItem( + CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY, + JSON.stringify(snapshot) + ); + } catch { + // Ignore session storage failures; the in-memory guard still handles normal tab switches. + } +} + function shouldShowClawdProcessingHint( message: string, hasTableFiles: boolean, @@ -1650,6 +1732,7 @@ export function CanvasChatPanel({ [searchParams] ); const [input, setInput] = useState(""); + const inputRef = useRef(input); const [attachments, setAttachments] = useState([]); const [selectedModelId, setSelectedModelId] = useState( effectiveAgentEnabled ? "auto" : "" @@ -1762,11 +1845,11 @@ export function CanvasChatPanel({ const historyScrollTimerRef = useRef | null>(null); const forceHistoryHydrationRef = useRef(null); const copiedUserMessageRef = useRef(null); - const pendingClawdSubmissionRef = useRef<{ - chatId: string; - submittedTexts: string[]; - startedAt: number; - } | null>(null); + const composerUserEditedAtRef = useRef(0); + const pendingClawdSubmissionRef = + useRef(null); + const recentClawdSubmissionRef = + useRef(null); const replaceChatSessionQuery = useCallback( (sessionId: string) => { @@ -2104,6 +2187,10 @@ export function CanvasChatPanel({ setSelectedSkillTagId(null); }, [activeChatId]); + useEffect(() => { + inputRef.current = input; + }, [input]); + const clearComposerEditor = useCallback(() => { setInput(""); const editableDiv = textareaRef.current as HTMLDivElement | null; @@ -2112,37 +2199,68 @@ export function CanvasChatPanel({ } }, []); - const clearRestoredClawdComposerText = useCallback(() => { + const getActiveClawdSubmissionSnapshot = useCallback(() => { + const now = Date.now(); const pendingSubmission = pendingClawdSubmissionRef.current; + if (pendingSubmission) { + return pendingSubmission; + } + + const recentSubmission = + recentClawdSubmissionRef.current || + readStoredClawdComposerSubmissionSnapshot(); + if (!recentSubmission) { + return null; + } + recentClawdSubmissionRef.current = recentSubmission; + + if (recentSubmission.expiresAt <= now) { + recentClawdSubmissionRef.current = null; + writeStoredClawdComposerSubmissionSnapshot(null); + return null; + } + + return recentSubmission; + }, []); + + const clearRestoredClawdComposerText = useCallback(() => { + const submissionSnapshot = getActiveClawdSubmissionSnapshot(); const editableDiv = textareaRef.current as HTMLDivElement | null; - if (!pendingSubmission || !editableDiv) return; - if (pendingSubmission.chatId !== activeChatIdRef.current) return; + if (!submissionSnapshot || !editableDiv) return; + if (submissionSnapshot.chatId !== activeChatIdRef.current) return; + if (composerUserEditedAtRef.current > submissionSnapshot.startedAt) return; const editorText = normalizeClawdComposerText( getComposerPlainTextFromElement(editableDiv) ); - if (!editorText) { - if (editableDiv.childNodes.length > 0) { + const stateText = normalizeClawdComposerText(inputRef.current); + if (!editorText && !stateText) { + if (pendingClawdSubmissionRef.current && editableDiv.childNodes.length > 0) { clearComposerEditor(); } return; } - const submittedTextSet = new Set( - pendingSubmission.submittedTexts - .map((text) => normalizeClawdComposerText(text)) - .filter(Boolean) + const editorWasRestored = isClawdSubmittedComposerText( + submissionSnapshot, + editorText ); - if (!submittedTextSet.has(editorText)) return; + const stateWasRestored = isClawdSubmittedComposerText( + submissionSnapshot, + stateText + ); + if (!editorWasRestored && !stateWasRestored) return; clearComposerEditor(); - }, [clearComposerEditor]); + }, [clearComposerEditor, getActiveClawdSubmissionSnapshot]); useEffect(() => { if (typeof window === "undefined") return; const clearAfterRestore = () => { - window.setTimeout(clearRestoredClawdComposerText, 0); + [0, 120, 500, 1500].forEach((delay) => { + window.setTimeout(clearRestoredClawdComposerText, delay); + }); }; const handleVisibilityChange = () => { if (document.visibilityState === "visible") { @@ -9452,16 +9570,22 @@ export function CanvasChatPanel({ signature: sendSignature, timestamp: Date.now(), }); - pendingClawdSubmissionRef.current = { + const clawdSubmissionStartedAt = Date.now(); + const clawdSubmissionSnapshot: ClawdComposerSubmissionSnapshot = { chatId: activeChatId, + expiresAt: + clawdSubmissionStartedAt + CLAWD_SUBMITTED_COMPOSER_RECOVERY_TTL_MS, + startedAt: clawdSubmissionStartedAt, submittedTexts: [ textContent, baseRequestMessage, requestMessage, optimisticMessageText, ], - startedAt: Date.now(), }; + pendingClawdSubmissionRef.current = clawdSubmissionSnapshot; + recentClawdSubmissionRef.current = clawdSubmissionSnapshot; + writeStoredClawdComposerSubmissionSnapshot(clawdSubmissionSnapshot); setClawdProcessingHint(nextProcessingHint); setIsClawdSubmitting(true); setMessages((previousMessages) => @@ -9471,6 +9595,9 @@ export function CanvasChatPanel({ ]) ); clearComposerEditor(); + [0, 120, 500, 1500].forEach((delay) => { + window.setTimeout(clearRestoredClawdComposerText, delay); + }); setAttachments([]); setClawdTableFiles([]); clearSelection(); @@ -9536,6 +9663,11 @@ export function CanvasChatPanel({ hasHydratedHistoryRef.current = true; } catch (error) { console.error("[CanvasChat] Claw request failed:", error); + pendingClawdSubmissionRef.current = null; + if (recentClawdSubmissionRef.current === clawdSubmissionSnapshot) { + recentClawdSubmissionRef.current = null; + writeStoredClawdComposerSubmissionSnapshot(null); + } setMessages((previousMessages) => previousMessages.filter((message) => message.id !== userMessageId) ); @@ -9565,6 +9697,14 @@ export function CanvasChatPanel({ } } finally { pendingClawdSubmissionRef.current = null; + if (recentClawdSubmissionRef.current === clawdSubmissionSnapshot) { + const refreshedSubmissionSnapshot = { + ...clawdSubmissionSnapshot, + expiresAt: Date.now() + CLAWD_SUBMITTED_COMPOSER_RECOVERY_TTL_MS, + }; + recentClawdSubmissionRef.current = refreshedSubmissionSnapshot; + writeStoredClawdComposerSubmissionSnapshot(refreshedSubmissionSnapshot); + } setIsClawdSubmitting(false); setClawdProcessingHint(null); releaseSendGuard(activeChatId); @@ -9899,6 +10039,7 @@ export function CanvasChatPanel({ setSendGuard, setMessages, clearComposerEditor, + clearRestoredClawdComposerText, shouldBlockDuplicateSend, status, mutate, @@ -13309,6 +13450,18 @@ export function CanvasChatPanel({ .replace(/\u200b/g, " ") .replace(/\s+/g, " ") .trim(); + const activeClawdSubmission = + getActiveClawdSubmissionSnapshot(); + const isRestoredSubmittedClawdText = + activeClawdSubmission !== null && + activeClawdSubmission.chatId === activeChatIdRef.current && + isClawdSubmittedComposerText( + activeClawdSubmission, + textContent + ); + if (!isRestoredSubmittedClawdText) { + composerUserEditedAtRef.current = Date.now(); + } setInput(textContent); if ((!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) { syncAttachmentsWithEditor(target);