diff --git a/frontend/src/components/workspace/workspace-header.tsx b/frontend/src/components/workspace/workspace-header.tsx index 036f9927..87a7272c 100644 --- a/frontend/src/components/workspace/workspace-header.tsx +++ b/frontend/src/components/workspace/workspace-header.tsx @@ -43,7 +43,7 @@ export function WorkspaceHeader({ className }: { className?: string }) { ) : (
{/* TODO: 测试标识 */} - XClaw v3.2.3 + XClaw v3.2.4
)} diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index c2bb5abe..08e712fe 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -49,6 +49,12 @@ export type LegacyThreadStreamOptions = { const STREAM_ERROR_FALLBACK_MESSAGE = "Request failed."; const STREAM_ERROR_TOAST_MESSAGE = "出现了某些错误。"; const STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS = 2000; +const STREAM_CANCEL_PATTERNS = [ + /\bcancellederror\b/i, + /\bcancelled\b/i, + /\bcanceled\b/i, + /\babort(?:ed|error)?\b/i, +]; function readMessageCandidate(value: unknown): string | null { if (typeof value === "string" && value.trim()) { @@ -116,6 +122,21 @@ function getStreamErrorMessage(error: unknown): string { return STREAM_ERROR_FALLBACK_MESSAGE; } +function isStreamCancellation(error: unknown, message: string): boolean { + const direct = + typeof error === "object" && + error !== null && + "name" in error && + typeof Reflect.get(error, "name") === "string" + ? String(Reflect.get(error, "name")) + : ""; + + const candidates = [message, direct]; + return candidates.some((value) => + STREAM_CANCEL_PATTERNS.some((pattern) => pattern.test(value)), + ); +} + function normalizeThreadId( value: string | null | undefined, ): string | undefined { @@ -234,6 +255,11 @@ export function useThreadStream({ const showStreamErrorToast = useCallback((error: unknown) => { const message = getStreamErrorMessage(error); + if (isStreamCancellation(error, message)) { + // Cancellation is expected when user presses "Stop" or stream disconnects. + console.info("[useThreadStream] stream cancelled:", message); + return; + } const now = Date.now(); const lastToast = lastErrorToastRef.current; if (