import {
	createContext,
	useCallback,
	useContext,
	useEffect,
	useMemo,
	useRef,
	useState,
	type ReactNode,
	type SetStateAction,
} from "react";
import * as chatApi from "../api/chat";
import * as sessionsApi from "../api/sessions";
import { apiUrl } from "../api/base";
import type { ExtensionDialogState, RetryState, SseEvent } from "../types/events";
import type { RunPhase, RunPhaseState } from "../types/runPhase";
import type { AssistantBlock, ChatMessage, ImageContent, ModelInfo, SessionState, ThinkingLevel, ToolCallBlock } from "../types/message";
import type { SessionHistoryPayload, SessionSummary } from "../types/session";
import { nextMessageId, syncViewportHeight, stripAnsi } from "../utils/format";
import { formatBashOutput, parseBashInput } from "../utils/bash";
import { buildHistoryMessages } from "../utils/historyMessages";
import {
	assistantPlainText,
	extractAssistantBlocks,
	hasTextBlock,
	hasThinkingBlock,
} from "../utils/assistantBlocks";
import {
	extractContent,
	extractImages,
	formatToolCall,
	getToolCalls,
	buildToolCallUpdateText,
	preferToolCallMessage,
} from "../utils/toolCall";
import {
	formatSessionTitle,
	isMachineSessionLabel,
	modelKey,
	titleFromFirstUserMessage,
} from "../utils/sessionTitle";

interface ChatContextValue {
	connected: boolean;
	isStreaming: boolean;
	messages: ChatMessage[];
	sessions: SessionSummary[];
	activeSessionPath: string | null;
	headerTitle: string;
	headerMeta: string;
	sidebarOpen: boolean;
	sessionState: SessionState | null;
	availableModels: ModelInfo[];
	currentModelKey: string;
	thinkingLevel: string;
	availableThinkingLevels: ThinkingLevel[];
	isApplyingModelSettings: boolean;
	pendingSteering: string[];
	pendingFollowUp: string[];
	retryState: RetryState | null;
	isCompacting: boolean;
	runPhase: RunPhaseState | null;
	extensionWidgets: Record<string, string[]>;
	extensionStatuses: Record<string, string>;
	extensionDialog: ExtensionDialogState | null;
	toolsExpanded: boolean;
	showThinkingAndTools: boolean;
	toggleSidebar: () => void;
	closeSidebar: () => void;
	loadSessions: () => Promise<void>;
	loadSession: (path: string) => Promise<void>;
	deleteSession: (path: string) => Promise<void>;
	renameSession: (path: string, currentTitle: string) => Promise<void>;
	toggleSessionPin: (path: string, pinned: boolean) => Promise<void>;
	newSession: () => void;
	newTemporarySession: () => void;
	isEphemeralSession: boolean;
	sendMessage: (
		text: string,
		images?: ImageContent[],
		options?: { mode?: "prompt" | "steer" | "follow_up" },
	) => Promise<void>;
	runBashCommand: (text: string) => Promise<void>;
	abortStream: () => Promise<void>;
	abortRetry: () => Promise<void>;
	toggleToolsExpanded: () => void;
	toggleShowThinkingAndTools: () => void;
	respondExtensionDialog: (response: Record<string, unknown>) => void;
	dismissExtensionDialog: () => void;
	applyModelSelection: (key: string) => Promise<void>;
	applyThinkingSelection: (level: string) => Promise<void>;
	addSystemMessage: (text: string) => void;
}

const ChatContext = createContext<ChatContextValue | null>(null);

const THINKING_LEVELS_LIST: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
const LAST_ACTIVE_SESSION_KEY = "webui:lastActiveSessionPath";
const EPHEMERAL_SESSION_ID = "__ephemeral__";
const EPHEMERAL_CLEANUP_KEY = "webui:ephemeralSessionPath";
const SHOW_THINKING_TOOLS_KEY = "webui:showThinkingAndTools";

function isActiveSessionBound(
	isEphemeral: boolean,
	activePath: string | null,
	backendPath: string | null,
): boolean {
	if (isEphemeral) {
		return Boolean(backendPath);
	}
	if (!activePath) return false;
	return backendPath === activePath;
}

/** Survives React StrictMode remount so boot only runs once per page load. */
let sharedSessionBootPromise: Promise<void> | null = null;

function resolvePersistSessionPath(backend: string | null, active: string | null): string | null {
	return backend || active;
}

function pickMostRecentSession(list: SessionSummary[]): SessionSummary | null {
	if (list.length === 0) return null;
	return [...list].sort(
		(a, b) =>
			new Date(String(b.modified || b.created || 0)).getTime() -
			new Date(String(a.modified || a.created || 0)).getTime(),
	)[0];
}

function getAvailableThinkingLevels(model: ModelInfo | undefined): ThinkingLevel[] {
	if (!model?.reasoning) return ["off"];
	return THINKING_LEVELS_LIST.filter((level) => {
		const mapped = model.thinkingLevelMap?.[level];
		if (mapped === null) return false;
		if (level === "xhigh") return mapped !== undefined;
		return true;
	});
}

function historyToMessages(payload: SessionHistoryPayload): ChatMessage[] {
	return normalizeChatMessages(buildHistoryMessages(payload));
}

function withoutStreamingTurnMessages(prev: ChatMessage[], assistantId: string | null): ChatMessage[] {
	return prev.filter((msg) => msg.id !== assistantId);
}

/** Index where the current turn begins (just after the last user message). */
function lastTurnStartIndex(messages: ChatMessage[]): number {
	for (let i = messages.length - 1; i >= 0; i--) {
		if (messages[i].role === "user") return i + 1;
	}
	return 0;
}

/** Drop still-streaming assistant bubbles in the current turn. */
function stripStreamingTurnBubbles(messages: ChatMessage[]): ChatMessage[] {
	const turnStart = lastTurnStartIndex(messages);
	return messages.filter((msg, i) => {
		if (i < turnStart) return true;
		if (!msg.streaming) return true;
		return msg.role !== "assistant";
	});
}

function findStreamingAssistantId(messages: ChatMessage[]): string | null {
	for (let i = messages.length - 1; i >= 0; i--) {
		const msg = messages[i];
		if (msg.role === "assistant" && msg.streaming) {
			return msg.id;
		}
	}
	return null;
}

function findLastTurnAssistantId(messages: ChatMessage[]): string | null {
	for (let i = messages.length - 1; i >= 0; i--) {
		const msg = messages[i];
		if (msg.role === "user") break;
		if (msg.role === "assistant") return msg.id;
	}
	return null;
}

function resolveTurnAssistantId(messages: ChatMessage[], refId: string | null): string | null {
	return refId ?? findStreamingAssistantId(messages) ?? findLastTurnAssistantId(messages);
}

function findLastTurnAssistantMessage(messages: ChatMessage[]): ChatMessage | null {
	for (let i = messages.length - 1; i >= 0; i--) {
		const msg = messages[i];
		if (msg.role === "user") break;
		if (msg.role === "assistant") return msg;
	}
	return null;
}

function appendStreamingAssistantTurn(
	prev: ChatMessage[],
	blocks: AssistantBlock[],
	assistantIdRef: { current: string | null },
): ChatMessage[] {
	if (!assistantIdRef.current) {
		assistantIdRef.current = findStreamingAssistantId(prev);
	}

	const next = stripStreamingTurnBubbles(prev);
	if (!assistantIdRef.current) {
		assistantIdRef.current = nextMessageId();
	}

	const id = assistantIdRef.current;
	return [
		...next,
		{
			id,
			role: "assistant" as const,
			blocks,
			content: assistantPlainText(blocks),
			streaming: true,
		},
	];
}

function finalizeStreamingTurn(prev: ChatMessage[]): ChatMessage[] {
	const turnStart = lastTurnStartIndex(prev);

	const survivors = prev.filter((msg, i) => {
		if (i < turnStart || msg.role !== "assistant" || !msg.streaming) return true;
		const shorter = msg.content.trim();
		if (!shorter) return true;
		for (let j = turnStart; j < prev.length; j++) {
			if (j === i) continue;
			const other = prev[j];
			if (other.role !== "assistant") continue;
			const longer = other.content.trim();
			if (longer.length > shorter.length && longer.startsWith(shorter)) {
				return false;
			}
		}
		return true;
	});

	const survivorTurnStart = lastTurnStartIndex(survivors);
	return survivors.map((msg, i) =>
		i >= survivorTurnStart && msg.streaming ? { ...msg, streaming: false } : msg,
	);
}

function applyAssistantMessageUpdate(
	prev: ChatMessage[],
	blocks: AssistantBlock[],
	assistantIdRef: { current: string | null },
): ChatMessage[] {
	const plainText = assistantPlainText(blocks);
	const lastAssistant = findLastTurnAssistantMessage(prev);
	if (lastAssistant && !lastAssistant.streaming) {
		const trimmed = plainText.trim();
		if (
			trimmed &&
			(lastAssistant.content === trimmed ||
				trimmed.startsWith(lastAssistant.content) ||
				lastAssistant.content.startsWith(trimmed))
		) {
			if (trimmed.length >= lastAssistant.content.length) {
				return prev.map((msg) =>
					msg.id === lastAssistant.id
						? { ...msg, blocks, content: plainText }
						: msg,
				);
			}
			return prev;
		}
	}
	return appendStreamingAssistantTurn(prev, blocks, assistantIdRef);
}

function finalizeAssistantMessageEnd(
	prev: ChatMessage[],
	blocks: AssistantBlock[],
	toolCalls: ToolCallBlock[],
	assistantId: string | null,
): ChatMessage[] {
	let next = withoutStreamingTurnMessages(prev, assistantId);

	const plainText = assistantPlainText(blocks).trim();
	if (plainText || blocks.some((b) => b.type === "thinking")) {
		const lastAssistant = findLastTurnAssistantMessage(next);
		if (!(lastAssistant && !lastAssistant.streaming && lastAssistant.content === plainText)) {
			next = [
				...next,
				{
					id: assistantId ?? nextMessageId(),
					role: "assistant" as const,
					blocks,
					content: assistantPlainText(blocks),
				},
			];
		}
	}

	for (const tc of toolCalls) {
		const tid = tc.toolCallId || tc.id;
		const tidStr = tid ? String(tid) : undefined;
		const text = formatToolCall(tc);
		const existingIdx = tidStr ? next.findIndex((msg) => msg.toolCallId === tidStr) : -1;
		if (existingIdx >= 0) {
			const cur = next[existingIdx].content.trimStart();
			const running = cur.startsWith("🔧") || cur.startsWith("✅") || cur.startsWith("❌");
			if (!running) {
				next = next.map((msg, i) => (i === existingIdx ? { ...msg, content: text } : msg));
			}
		} else {
			next = [
				...next,
				{
					id: tidStr ? `tool-${tidStr}` : nextMessageId(),
					role: "tool_call" as const,
					content: text,
					toolCallId: tidStr,
				},
			];
		}
	}
	return next;
}

function messageDedupeSignature(msg: ChatMessage): string | null {
	if (msg.role === "tool_call" && msg.toolCallId) {
		return `tool_call:${msg.toolCallId}`;
	}
	if (msg.role === "thinking") {
		const content = msg.content.trim().replace(/\s+/g, " ");
		return content ? `thinking:${content}` : null;
	}
	if (msg.role !== "assistant" && msg.role !== "tool_call") {
		return null;
	}
	const content = msg.content.trim().replace(/\s+/g, " ");
	if (!content && !msg.blocks?.length) return null;
	return `${msg.role}:${msg.toolCallId ?? ""}:${content}`;
}

function normalizeContent(content: string): string {
	return content.trim().replace(/\s+/g, " ");
}

/**
 * Drop streaming assistant bubbles whose content is just a prefix of another
 * assistant bubble in the same turn. This clears orphaned streaming bubbles
 * (the duplicate-message symptom) while leaving genuinely distinct assistant
 * replies untouched, since a real pre-tool reply is already non-streaming by
 * the time the next reply starts streaming.
 */
function collapseStreamingPrefixOrphans(messages: ChatMessage[]): ChatMessage[] {
	let turnAssistants: { index: number; content: string }[] = [];
	const dropIndices = new Set<number>();

	const flushTurn = () => {
		for (const a of turnAssistants) {
			if (!a.content) continue;
			const orphan = turnAssistants.some(
				(b) =>
					b.index !== a.index &&
					b.content.length > a.content.length &&
					b.content.startsWith(a.content),
			);
			if (orphan && messages[a.index].streaming) {
				dropIndices.add(a.index);
			}
		}
		turnAssistants = [];
	};

	messages.forEach((msg, i) => {
		if (msg.role === "user") {
			flushTurn();
			return;
		}
		if (msg.role === "assistant") {
			turnAssistants.push({ index: i, content: normalizeContent(msg.content) });
		}
	});
	flushTurn();

	if (dropIndices.size === 0) return messages;
	return messages.filter((_, i) => !dropIndices.has(i));
}

function normalizeChatMessages(messages: ChatMessage[]): ChatMessage[] {
	const result: ChatMessage[] = [];
	let turnSignatures = new Map<string, number>();

	for (const msg of messages) {
		if (msg.role === "user") {
			turnSignatures = new Map();
			result.push(msg);
			continue;
		}

		const signature = messageDedupeSignature(msg);
		if (!signature) {
			result.push(msg);
			continue;
		}

		const existingIndex = turnSignatures.get(signature);
		if (existingIndex === undefined) {
			if (msg.role === "tool_call") {
				const idIdx = result.findIndex((m) => m.id === msg.id);
				if (idIdx >= 0) {
					result[idIdx] = preferToolCallMessage(result[idIdx], msg);
					turnSignatures.set(signature, idIdx);
					continue;
				}
			}
			turnSignatures.set(signature, result.length);
			result.push(msg);
			continue;
		}

		const existing = result[existingIndex];
		if (msg.role === "tool_call") {
			result[existingIndex] = preferToolCallMessage(existing, msg);
			continue;
		}
		if (existing.streaming && !msg.streaming) {
			result[existingIndex] = msg;
		}
	}

	return collapseStreamingPrefixOrphans(result);
}

function isIgnorableExtensionError(error: string | undefined): boolean {
	if (!error) return false;
	return error.includes("extension ctx is stale after session replacement or reload");
}

export function ChatProvider({ children }: { children: ReactNode }) {
	const [connected, setConnected] = useState(false);
	const [isStreaming, setIsStreaming] = useState(false);
	const [messages, setRawMessages] = useState<ChatMessage[]>([]);
	const [sessions, setSessions] = useState<SessionSummary[]>([]);
	const [activeSessionPath, setActiveSessionPath] = useState<string | null>(null);
	const [backendSessionPath, setBackendSessionPath] = useState<string | null>(null);
	const [headerTitle, setHeaderTitle] = useState("聊天");
	const [headerMeta, setHeaderMeta] = useState("");
	const [sidebarOpen, setSidebarOpen] = useState(false);
	const [sessionState, setSessionState] = useState<SessionState | null>(null);
	const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
	const [currentModelKey, setCurrentModelKey] = useState("");
	const [thinkingLevel, setThinkingLevel] = useState("off");
	const [isApplyingModelSettings, setIsApplyingModelSettings] = useState(false);
	const [pendingSteering, setPendingSteering] = useState<string[]>([]);
	const [pendingFollowUp, setPendingFollowUp] = useState<string[]>([]);
	const [retryState, setRetryState] = useState<RetryState | null>(null);
	const [isCompacting, setIsCompacting] = useState(false);
	const [runPhase, setRunPhase] = useState<RunPhaseState | null>(null);
	const [extensionWidgets, setExtensionWidgets] = useState<Record<string, string[]>>({});
	const [extensionStatuses, setExtensionStatuses] = useState<Record<string, string>>({});
	const [extensionDialog, setExtensionDialog] = useState<ExtensionDialogState | null>(null);
	const [toolsExpanded, setToolsExpanded] = useState(false);
	const [showThinkingAndTools, setShowThinkingAndTools] = useState(() => {
		try {
			const stored = localStorage.getItem(SHOW_THINKING_TOOLS_KEY);
			if (stored === null) return true;
			return stored === "true";
		} catch {
			return true;
		}
	});
	const [isEphemeralSession, setIsEphemeralSession] = useState(false);

	const sessionCache = useRef(new Map<string, SessionHistoryPayload>());
	const isEphemeralRef = useRef(false);
	const ephemeralSessionPathRef = useRef<string | null>(null);
	const suppressStreamMessagesRef = useRef(false);
	const sessionActivationPromise = useRef<Promise<SessionState | null> | null>(null);
	const newSessionPromise = useRef<Promise<void> | null>(null);
	const ensureSessionCreatePromise = useRef<Promise<boolean> | null>(null);
	const sessionBootRef = useRef<Promise<void> | null>(null);
	const streamingAssistantId = useRef<string | null>(null);
	const optimisticUserIdRef = useRef<string | null>(null);
	const compactionReloadPendingRef = useRef(false);
	const activeBashMsgIdRef = useRef<string | null>(null);
	const sessionDashboardRaf = useRef<number | null>(null);
	const pendingAssistantBlocksRef = useRef<AssistantBlock[] | null>(null);
	const messageUpdateRafRef = useRef<number | null>(null);
	const pendingBashOutputRef = useRef<string | null>(null);
	const bashUpdateRafRef = useRef<number | null>(null);
	const backendSessionPathRef = useRef(backendSessionPath);
	const activeSessionPathRef = useRef(activeSessionPath);
	const isStreamingRef = useRef(isStreaming);

	useEffect(() => {
		backendSessionPathRef.current = backendSessionPath;
	}, [backendSessionPath]);
	useEffect(() => {
		activeSessionPathRef.current = activeSessionPath;
	}, [activeSessionPath]);
	useEffect(() => {
		isStreamingRef.current = isStreaming;
	}, [isStreaming]);

	const setMessages = useCallback(
		(next: SetStateAction<ChatMessage[]>, options?: { skipNormalize?: boolean }) => {
			setRawMessages((prev) => {
				const resolved = typeof next === "function" ? next(prev) : next;
				return options?.skipNormalize ? resolved : normalizeChatMessages(resolved);
			});
		},
		[],
	);

	const flushPendingStreamUpdates = useCallback(() => {
		if (messageUpdateRafRef.current != null) {
			cancelAnimationFrame(messageUpdateRafRef.current);
			messageUpdateRafRef.current = null;
		}
		if (bashUpdateRafRef.current != null) {
			cancelAnimationFrame(bashUpdateRafRef.current);
			bashUpdateRafRef.current = null;
		}

		const blocks = pendingAssistantBlocksRef.current;
		const bashOutput = pendingBashOutputRef.current;
		pendingAssistantBlocksRef.current = null;
		pendingBashOutputRef.current = null;

		if (blocks) {
			setMessages(
				(prev) => applyAssistantMessageUpdate(prev, blocks, streamingAssistantId),
				{ skipNormalize: true },
			);
		}
		if (bashOutput != null) {
			const bashId = activeBashMsgIdRef.current;
			if (bashId) {
				setMessages(
					(prev) =>
						prev.map((msg) =>
							msg.id === bashId ? { ...msg, content: bashOutput, streaming: true } : msg,
						),
					{ skipNormalize: true },
				);
			}
		}
	}, [setMessages]);

	const scheduleAssistantMessageUpdate = useCallback(
		(blocks: AssistantBlock[]) => {
			pendingAssistantBlocksRef.current = blocks;
			if (messageUpdateRafRef.current != null) return;
			messageUpdateRafRef.current = requestAnimationFrame(() => {
				messageUpdateRafRef.current = null;
				const pending = pendingAssistantBlocksRef.current;
				if (!pending) return;
				pendingAssistantBlocksRef.current = null;
				setMessages(
					(prev) => applyAssistantMessageUpdate(prev, pending, streamingAssistantId),
					{ skipNormalize: true },
				);
			});
		},
		[setMessages],
	);

	const scheduleBashOutputUpdate = useCallback(
		(output: string) => {
			pendingBashOutputRef.current = output;
			if (bashUpdateRafRef.current != null) return;
			bashUpdateRafRef.current = requestAnimationFrame(() => {
				bashUpdateRafRef.current = null;
				const pending = pendingBashOutputRef.current;
				if (pending == null) return;
				pendingBashOutputRef.current = null;
				const bashId = activeBashMsgIdRef.current;
				if (!bashId) return;
				setMessages(
					(prev) =>
						prev.map((msg) =>
							msg.id === bashId ? { ...msg, content: pending, streaming: true } : msg,
						),
					{ skipNormalize: true },
				);
			});
		},
		[setMessages],
	);

	const availableThinkingLevels = useMemo(
		() => getAvailableThinkingLevels(sessionState?.model),
		[sessionState?.model],
	);

	const clearRunPhase = useCallback(() => {
		setRunPhase(null);
	}, []);

	const updateRunPhase = useCallback((phase: RunPhase, detail?: string) => {
		setRunPhase((prev) => {
			if (prev?.phase === phase && prev?.detail === detail) return prev;
			return { phase, detail, startedAt: Date.now() };
		});
	}, []);

	const resetStreamingState = useCallback(() => {
		streamingAssistantId.current = null;
		optimisticUserIdRef.current = null;
		clearRunPhase();
	}, [clearRunPhase]);

	const invalidateTurnEvents = useCallback(() => {
		resetStreamingState();
	}, [resetStreamingState]);

	const addSystemMessage = useCallback((text: string) => {
		setMessages((prev) => [...prev, { id: nextMessageId(), role: "system", content: text }]);
	}, []);

	const persistLastActiveSession = useCallback((path: string | null) => {
		try {
			if (path) {
				sessionStorage.setItem(LAST_ACTIVE_SESSION_KEY, path);
			} else {
				sessionStorage.removeItem(LAST_ACTIVE_SESSION_KEY);
			}
		} catch {
			/* ignore persistence errors */
		}
	}, []);

	const scheduleSessionDashboardRefresh = useCallback(() => {
		if (sessionDashboardRaf.current != null) return;
		sessionDashboardRaf.current = requestAnimationFrame(() => {
			sessionDashboardRaf.current = null;
			void chatApi.fetchSessionState().then((data) => {
				if (data.error) return;
				setSessionState(data);
				if (data.model) setCurrentModelKey(modelKey(data.model));
				if (data.thinkingLevel) setThinkingLevel(data.thinkingLevel);
				if (data.isStreaming !== undefined) setIsStreaming(data.isStreaming);

				if (!isActiveSessionBound(isEphemeralRef.current, activeSessionPathRef.current, backendSessionPathRef.current)) {
					return;
				}

				const sid = data.sessionId != null ? String(data.sessionId) : "";
				setSessions((currentSessions) => {
					const pathKey = isEphemeralRef.current ? backendSessionPathRef.current : activeSessionPathRef.current;
					const row = pathKey ? currentSessions.find((s) => s.path === pathKey) : undefined;
					if (row?.name && !isMachineSessionLabel(row.name, sid)) {
						setHeaderTitle(formatSessionTitle(row.name));
					} else if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) {
						setHeaderTitle(formatSessionTitle(data.sessionName));
					}
					return currentSessions;
				});
			});
		});
	}, []);

	const applySessionState = useCallback((data: SessionState) => {
		if (data.error) return;
		setSessionState(data);
		if (data.model) setCurrentModelKey(modelKey(data.model));
		if (data.thinkingLevel) setThinkingLevel(data.thinkingLevel);
		if (data.isStreaming !== undefined) setIsStreaming(data.isStreaming);
		if (data.isCompacting !== undefined) setIsCompacting(data.isCompacting);

		if (!isActiveSessionBound(isEphemeralRef.current, activeSessionPathRef.current, backendSessionPathRef.current)) {
			return;
		}

		const sid = data.sessionId != null ? String(data.sessionId) : "";
		const pathKey = isEphemeralRef.current ? backendSessionPathRef.current : activeSessionPathRef.current;
		setSessions((currentSessions) => {
			const row = pathKey ? currentSessions.find((s) => s.path === pathKey) : undefined;
			if (row?.name && !isMachineSessionLabel(row.name, sid)) {
				setHeaderTitle(formatSessionTitle(row.name));
			} else if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) {
				setHeaderTitle(formatSessionTitle(data.sessionName));
			} else if (isEphemeralRef.current) {
				setHeaderTitle("临时对话");
			} else if (row?.firstMessage && row.firstMessage !== "(空)") {
				setHeaderTitle(formatSessionTitle(titleFromFirstUserMessage(row.firstMessage)));
			}
			return currentSessions;
		});
	}, []);

	const fetchSessionStateFull = useCallback(async () => {
		try {
			const data = await chatApi.fetchSessionState();
			applySessionState(data);
		} catch {
			/* ignore */
		}
	}, [applySessionState]);

	const activateSessionBackend = useCallback(async (path: string): Promise<SessionState | null> => {
		const state = await sessionsApi.activateSessionBackend(path);
		setBackendSessionPath(path);
		return state;
	}, []);

	const syncBackendSessionPathFromState = useCallback(async (): Promise<string | null> => {
		const data = await chatApi.fetchSessionState();
		if (data.error) return null;
		const path = data.sessionFile?.trim() || null;
		if (path) {
			setBackendSessionPath(path);
		}
		return path;
	}, []);

	const loadSessions = useCallback(async () => {
		try {
			const data = await sessionsApi.fetchSessions();
			const ephemeralPath = ephemeralSessionPathRef.current;
			const list = (data.sessions || []).filter((s) => s.path !== ephemeralPath);
			setSessions(list);
		} catch (err) {
			addSystemMessage(`会话列表加载失败: ${err instanceof Error ? err.message : String(err)}`);
		}
	}, [addSystemMessage]);

	const cleanupEphemeralSessionOnBoot = useCallback(async () => {
		try {
			const path = sessionStorage.getItem(EPHEMERAL_CLEANUP_KEY)?.trim();
			if (!path) return;
			sessionStorage.removeItem(EPHEMERAL_CLEANUP_KEY);
			await sessionsApi.deleteSession(path);
		} catch {
			/* ignore cleanup errors */
		}
	}, []);

	const discardEphemeralSession = useCallback(async () => {
		if (!isEphemeralRef.current && !ephemeralSessionPathRef.current) return;

		const path = ephemeralSessionPathRef.current;
		isEphemeralRef.current = false;
		ephemeralSessionPathRef.current = null;
		setIsEphemeralSession(false);

		try {
			sessionStorage.removeItem(EPHEMERAL_CLEANUP_KEY);
		} catch {
			/* ignore */
		}

		if (!path) return;

		try {
			await sessionsApi.deleteSession(path);
		} catch {
			/* ignore delete errors */
		}
		sessionCache.current.delete(path);
		setSessions((prev) => prev.filter((s) => s.path !== path));
	}, []);

	const createAndBindNewSession = useCallback(
		async (options?: { persist?: boolean; refreshList?: boolean }): Promise<string> => {
			const { persist = true, refreshList = true } = options ?? {};
			const data = await chatApi.createNewSession();
			if (data.error) {
				throw new Error(data.error);
			}
			const path = data.sessionFile?.trim() || (await syncBackendSessionPathFromState());
			if (!path) {
				throw new Error("无法获取会话路径");
			}
			setBackendSessionPath(path);
			if (persist) {
				void persistLastActiveSession(path);
			}
			if (refreshList) {
				void loadSessions();
			}
			return path;
		},
		[syncBackendSessionPathFromState, persistLastActiveSession, loadSessions],
	);

	const syncMessagesForPath = useCallback(async (path: string) => {
		try {
			const payload = await sessionsApi.fetchSessionHistory(path);
			if (payload.error) {
				invalidateTurnEvents();
				setMessages([]);
				return;
			}
			sessionCache.current.set(path, payload);
			invalidateTurnEvents();
			setMessages(historyToMessages(payload));
		} catch {
			invalidateTurnEvents();
			setMessages([]);
		}
	}, [invalidateTurnEvents]);

	const clearChat = useCallback(
		(title = "聊天") => {
			invalidateTurnEvents();
			setMessages([]);
			setActiveSessionPath(null);
			setHeaderTitle(title);
			setSessionState(null);
		},
		[invalidateTurnEvents],
	);

	const loadSession = useCallback(
		async (path: string, options?: { skipDashboardRefresh?: boolean }) => {
			await discardEphemeralSession();

			if (!compactionReloadPendingRef.current) {
				invalidateTurnEvents();
			}
			setSidebarOpen(false);
			setActiveSessionPath(path);
			setHeaderMeta("加载中...");

			try {
				let payload = sessionCache.current.get(path);
				const poisonName = (p: SessionHistoryPayload) =>
					typeof p?.session?.name === "string" &&
					p.session.name.trim() &&
					isMachineSessionLabel(p.session.name.trim(), String(p.session?.id ?? ""));
				if (payload && poisonName(payload)) {
					sessionCache.current.delete(path);
					payload = undefined;
				}
				if (!payload) {
					payload = await sessionsApi.fetchSessionHistory(path);
					if (payload.error) throw new Error(payload.error);
					sessionCache.current.set(path, payload);
				}

				const sid = payload.session?.id != null ? String(payload.session.id) : "";
				let headerLabel = "";
				if (
					typeof payload.session?.name === "string" &&
					payload.session.name.trim() &&
					!isMachineSessionLabel(payload.session.name.trim(), sid)
				) {
					headerLabel = payload.session.name.trim();
				}
				if (!headerLabel) {
					for (const m of payload.messages || []) {
						if (m.role === "user") {
							const fb = titleFromFirstUserMessage(extractContent(m.content));
							if (fb) {
								headerLabel = fb;
								break;
							}
							if (extractImages(m.content).length) {
								headerLabel = `[${extractImages(m.content).length} 张图片]`;
								break;
							}
						}
					}
				}
				setHeaderTitle(formatSessionTitle(headerLabel));
				setMessages(historyToMessages(payload));
				setHeaderMeta("正在切换会话…");
				try {
					const activationPromise = activateSessionBackend(path);
					sessionActivationPromise.current = activationPromise;
					await activationPromise;
					await fetchSessionStateFull();
				} catch (err) {
					addSystemMessage(`切换会话失败: ${err instanceof Error ? err.message : String(err)}`);
				} finally {
					sessionActivationPromise.current = null;
					setHeaderMeta("");
				}
				void persistLastActiveSession(path);
				if (!options?.skipDashboardRefresh) {
					scheduleSessionDashboardRefresh();
				}
			} catch (err) {
				setHeaderMeta("");
				addSystemMessage(`加载会话失败: ${err instanceof Error ? err.message : String(err)}`);
			}
		},
		[addSystemMessage, persistLastActiveSession, scheduleSessionDashboardRefresh, invalidateTurnEvents, activateSessionBackend, fetchSessionStateFull, discardEphemeralSession],
	);

	const restoreSessionOnBoot = useCallback((onSessionsReady?: () => void) => {
		const notifySessionsReady = () => onSessionsReady?.();
		if (!sharedSessionBootPromise) {
			sharedSessionBootPromise = (async () => {
				await cleanupEphemeralSessionOnBoot();

				const data = await sessionsApi.fetchSessions();
				const list = data.sessions || [];
				setSessions(list);
				notifySessionsReady();

				let targetPath: string | null = null;
				try {
					const saved = sessionStorage.getItem(LAST_ACTIVE_SESSION_KEY)?.trim();
					if (saved && list.some((s) => s.path === saved)) {
						targetPath = saved;
					}
				} catch {
					/* ignore sessionStorage read errors */
				}

				if (!targetPath) {
					targetPath = pickMostRecentSession(list)?.path ?? null;
				}

				if (targetPath) {
					await loadSession(targetPath, { skipDashboardRefresh: true });
					return;
				}

				await createAndBindNewSession();
				invalidateTurnEvents();
				setActiveSessionPath(null);
				setMessages([]);
				setHeaderTitle("聊天");
				sessionActivationPromise.current = null;
				await fetchSessionStateFull();
			})().catch((err) => {
				sharedSessionBootPromise = null;
				addSystemMessage(`初始化会话失败: ${err instanceof Error ? err.message : String(err)}`);
				throw err;
			});
		} else if (onSessionsReady) {
			void sharedSessionBootPromise.then(notifySessionsReady).catch(notifySessionsReady);
		}
		sessionBootRef.current = sharedSessionBootPromise;
		return sharedSessionBootPromise;
	}, [
		loadSession,
		fetchSessionStateFull,
		addSystemMessage,
		createAndBindNewSession,
		invalidateTurnEvents,
		cleanupEphemeralSessionOnBoot,
	]);

	const deleteSessionHandler = useCallback(
		async (path: string) => {
			if (isStreamingRef.current) {
				addSystemMessage("正在回复中，暂不能删除会话");
				return;
			}
			const row = sessions.find((s) => s.path === path);
			const title = formatSessionTitle(row?.name || row?.firstMessage || "该会话");
			if (!confirm(`删除会话「${title}」？此操作不可恢复。`)) return;

			try {
				await sessionsApi.deleteSession(path);
				sessionCache.current.delete(path);
				setSessions((prev) => prev.filter((s) => s.path !== path));
				if (activeSessionPathRef.current === path || (isEphemeralRef.current && ephemeralSessionPathRef.current === path)) {
					clearChat();
					setBackendSessionPath(null);
					sessionActivationPromise.current = null;
					void persistLastActiveSession(null);
				}
			} catch (err) {
				addSystemMessage(`删除会话失败: ${err instanceof Error ? err.message : String(err)}`);
			}
		},
		[sessions, addSystemMessage, clearChat, persistLastActiveSession],
	);

	const renameSessionHandler = useCallback(
		async (path: string, newName: string) => {
			if (isStreamingRef.current) {
				addSystemMessage("正在回复中，暂不能重命名会话");
				return;
			}
			const trimmed = newName.trim();
			if (!trimmed) {
				addSystemMessage("会话名称不能为空");
				return;
			}

			try {
				await sessionsApi.renameSession(path, trimmed);
				sessionCache.current.delete(path);
				setSessions((prev) =>
					prev.map((s) => (s.path === path ? { ...s, name: trimmed } : s)),
				);
				if (activeSessionPathRef.current === path) {
					setHeaderTitle(formatSessionTitle(trimmed));
				}
			} catch (err) {
				addSystemMessage(`重命名失败: ${err instanceof Error ? err.message : String(err)}`);
			}
		},
		[sessions, addSystemMessage],
	);

	const toggleSessionPinHandler = useCallback(
		async (path: string, pinned: boolean) => {
			if (isStreamingRef.current) {
				addSystemMessage("正在回复中，暂不能修改置顶");
				return;
			}

			try {
				await sessionsApi.setSessionPinned(path, pinned);
				await loadSessions();
			} catch (err) {
				addSystemMessage(`${pinned ? "置顶" : "取消置顶"}失败: ${err instanceof Error ? err.message : String(err)}`);
			}
		},
		[addSystemMessage, loadSessions],
	);

	const newSession = useCallback(() => {
		if (isStreamingRef.current) {
			addSystemMessage("正在回复中，暂不能创建新会话");
			return;
		}
		if (newSessionPromise.current) return;

		const promise = (async () => {
			await discardEphemeralSession();

			suppressStreamMessagesRef.current = true;
			setBackendSessionPath(null);
			setActiveSessionPath(null);
			sessionActivationPromise.current = null;
			setHeaderMeta("");
			clearChat("新会话");
			void persistLastActiveSession(null);

			const path = await createAndBindNewSession();
			await syncMessagesForPath(path);
			await fetchSessionStateFull();
		})();

		newSessionPromise.current = promise;
		void promise
			.catch((err) => {
				setHeaderMeta("");
				addSystemMessage(`创建新会话失败: ${err instanceof Error ? err.message : String(err)}`);
			})
			.finally(() => {
				suppressStreamMessagesRef.current = false;
				newSessionPromise.current = null;
			});
	}, [addSystemMessage, clearChat, createAndBindNewSession, discardEphemeralSession, fetchSessionStateFull, persistLastActiveSession, syncMessagesForPath]);

	const newTemporarySession = useCallback(() => {
		if (isStreamingRef.current) {
			addSystemMessage("正在回复中，暂不能创建临时对话");
			return;
		}
		if (newSessionPromise.current) return;

		const promise = (async () => {
			await discardEphemeralSession();

			suppressStreamMessagesRef.current = true;
			setSidebarOpen(false);
			setBackendSessionPath(null);
			sessionActivationPromise.current = null;
			setHeaderMeta("");
			clearChat("临时对话");
			void persistLastActiveSession(null);

			const path = await createAndBindNewSession({ persist: false, refreshList: false });

			isEphemeralRef.current = true;
			ephemeralSessionPathRef.current = path;
			setIsEphemeralSession(true);
			setActiveSessionPath(EPHEMERAL_SESSION_ID);

			try {
				sessionStorage.setItem(EPHEMERAL_CLEANUP_KEY, path);
			} catch {
				/* ignore */
			}

			await syncMessagesForPath(path);
			await fetchSessionStateFull();
		})();

		newSessionPromise.current = promise;
		void promise
			.catch((err) => {
				isEphemeralRef.current = false;
				ephemeralSessionPathRef.current = null;
				setIsEphemeralSession(false);
				setHeaderMeta("");
				addSystemMessage(`创建临时对话失败: ${err instanceof Error ? err.message : String(err)}`);
			})
			.finally(() => {
				suppressStreamMessagesRef.current = false;
				newSessionPromise.current = null;
			});
	}, [
		addSystemMessage,
		clearChat,
		createAndBindNewSession,
		discardEphemeralSession,
		fetchSessionStateFull,
		persistLastActiveSession,
		syncMessagesForPath,
	]);

	const ensureSessionReady = useCallback(async (): Promise<boolean> => {
		if (newSessionPromise.current) {
			try {
				await newSessionPromise.current;
			} catch {
				return false;
			}
		}

		if (sessionBootRef.current) {
			try {
				await sessionBootRef.current;
			} catch {
				return false;
			}
		}

		if (!backendSessionPathRef.current && !activeSessionPathRef.current) {
			if (!ensureSessionCreatePromise.current) {
				ensureSessionCreatePromise.current = (async () => {
					try {
						await createAndBindNewSession();
						return true;
					} catch (err) {
						addSystemMessage(`创建新会话失败: ${err instanceof Error ? err.message : String(err)}`);
						return false;
					} finally {
						ensureSessionCreatePromise.current = null;
					}
				})();
			}
			const created = await ensureSessionCreatePromise.current;
			if (!created) return false;
		}

		if (
			!isEphemeralRef.current &&
			activeSessionPathRef.current &&
			backendSessionPathRef.current !== activeSessionPathRef.current
		) {
			setHeaderMeta("正在切换会话...");
			if (sessionActivationPromise.current) {
				try {
					await sessionActivationPromise.current;
				} catch {
					setHeaderMeta("");
					return false;
				}
			} else {
				try {
					const activationPromise = activateSessionBackend(activeSessionPathRef.current);
					sessionActivationPromise.current = activationPromise;
					await activationPromise;
				} catch (err) {
					setHeaderMeta("");
					addSystemMessage(`切换会话失败: ${err instanceof Error ? err.message : String(err)}`);
					return false;
				} finally {
					sessionActivationPromise.current = null;
				}
			}
			setHeaderMeta("");
		}

		return true;
	}, [activateSessionBackend, addSystemMessage, createAndBindNewSession]);

	const handleSlashChatResponse = useCallback(
		async (response: Awaited<ReturnType<typeof chatApi.sendChat>>) => {
			if (response.action === "copy" && response.message) {
				resetStreamingState();
				try {
					await navigator.clipboard.writeText(response.message);
					addSystemMessage("已复制上一条助手消息到剪贴板");
				} catch {
					addSystemMessage(response.message);
				}
			} else if (response.message && response.action !== "reload_messages") {
				addSystemMessage(response.message);
			}

			if (response.action === "new_session") {
				const path = response.sessionFile?.trim();
				if (path) {
					await loadSession(path);
				} else {
					await discardEphemeralSession();
					clearChat();
					await fetchSessionStateFull();
				}
			} else if (response.action === "reload_messages") {
				const path = activeSessionPathRef.current;
				if (path) {
					compactionReloadPendingRef.current = true;
					sessionCache.current.delete(path);
					await loadSession(path);
					compactionReloadPendingRef.current = false;
				}
			} else if (response.action === "reload_sessions") {
				await loadSessions();
				await fetchSessionStateFull();
			}
		},
		[
			addSystemMessage,
			clearChat,
			discardEphemeralSession,
			loadSessions,
			loadSession,
			fetchSessionStateFull,
			resetStreamingState,
			persistLastActiveSession,
		],
	);

	const sendMessage = useCallback(
		async (text: string, images?: ImageContent[], options?: { mode?: "prompt" | "steer" | "follow_up" }) => {
			const trimmed = text.trim();
			const hasImages = Array.isArray(images) && images.length > 0;
			const isSlash = trimmed.startsWith("/");
			const mode = options?.mode ?? "prompt";
			if (!trimmed && !hasImages) return;

			if (!(await ensureSessionReady())) return;

			if (isStreamingRef.current && !isSlash) {
				setMessages((prev) => [
					...prev,
					{
						id: nextMessageId(),
						role: "user",
						content: trimmed,
						...(hasImages ? { images } : {}),
					},
				]);
				try {
					if (mode === "follow_up") {
						await chatApi.sendFollowUp(trimmed, hasImages ? images : undefined);
					} else {
						await chatApi.sendSteer(trimmed, hasImages ? images : undefined);
					}
				} catch (err) {
					addSystemMessage(`发送失败: ${err instanceof Error ? err.message : String(err)}`);
				}
				return;
			}

			const skipOptimisticUser = isSlash;

			if (!skipOptimisticUser) {
				const userId = nextMessageId();
				optimisticUserIdRef.current = userId;
				setMessages((prev) => [
					...prev,
					{
						id: userId,
						role: "user",
						content: trimmed,
						...(hasImages ? { images } : {}),
					},
				]);
			}

			const persistPathAfterSend = () => {
				if (isEphemeralRef.current) return;
				const persistPath = resolvePersistSessionPath(
					backendSessionPathRef.current,
					activeSessionPathRef.current,
				);
				if (persistPath) {
					persistLastActiveSession(persistPath);
				}
			};

			if (isSlash) {
				try {
					const response = await chatApi.sendChat(trimmed, hasImages ? images : undefined);
					await handleSlashChatResponse(response);
					persistPathAfterSend();
				} catch (err) {
					invalidateTurnEvents();
					const failedId = optimisticUserIdRef.current;
					optimisticUserIdRef.current = null;
					if (failedId) {
						setMessages((prev) => prev.filter((msg) => msg.id !== failedId));
					}
					addSystemMessage(`发送失败: ${err instanceof Error ? err.message : String(err)}`);
				}
				return;
			}

			void chatApi
				.sendChat(trimmed, hasImages ? images : undefined)
				.then((response) => {
					persistPathAfterSend();
					if (response.slash) {
						void handleSlashChatResponse(response);
					}
				})
				.catch((err) => {
					invalidateTurnEvents();
					const failedId = optimisticUserIdRef.current;
					optimisticUserIdRef.current = null;
					if (failedId) {
						setMessages((prev) => prev.filter((msg) => msg.id !== failedId));
					}
					addSystemMessage(`发送失败: ${err instanceof Error ? err.message : String(err)}`);
				});
		},
		[
			ensureSessionReady,
			addSystemMessage,
			persistLastActiveSession,
			handleSlashChatResponse,
			invalidateTurnEvents,
		],
	);

	const runBashCommand = useCallback(
		async (rawInput: string) => {
			const parsed = parseBashInput(rawInput);
			if (!parsed) return;

			if (!(await ensureSessionReady())) return;

			const bashMsgId = nextMessageId();
			activeBashMsgIdRef.current = bashMsgId;
			setMessages((prev) => [
				...prev,
				{ id: nextMessageId(), role: "user", content: parsed.display },
				{
					id: bashMsgId,
					role: "bash",
					content: "",
					bashCommand: parsed.command,
					bashExcludeFromContext: parsed.excludeFromContext,
					streaming: true,
				},
			]);

			try {
				const result = await chatApi.runBash(parsed.command, {
					excludeFromContext: parsed.excludeFromContext,
				});
				activeBashMsgIdRef.current = null;
				setMessages((prev) =>
					prev.map((msg) =>
						msg.id === bashMsgId
							? {
									...msg,
									content: formatBashOutput(result),
									bashExitCode: result.exitCode,
									streaming: false,
								}
							: msg,
					),
				);
				const persistPath = resolvePersistSessionPath(
					backendSessionPathRef.current,
					activeSessionPathRef.current,
				);
				if (persistPath) {
					sessionCache.current.delete(persistPath);
					void persistLastActiveSession(persistPath);
				}
			} catch (err) {
				activeBashMsgIdRef.current = null;
				setMessages((prev) =>
					prev.map((msg) =>
						msg.id === bashMsgId
							? {
									...msg,
									content: `执行失败: ${err instanceof Error ? err.message : String(err)}`,
									streaming: false,
								}
							: msg,
					),
				);
			}
		},
		[ensureSessionReady, persistLastActiveSession],
	);

	const abortStream = useCallback(async () => {
		if (!isStreamingRef.current && !isCompacting) return;
		try {
			await chatApi.abortChat();
		} catch {
			/* ignore */
		}
	}, [isCompacting]);

	const abortRetry = useCallback(async () => {
		try {
			await chatApi.abortRetry();
			setRetryState(null);
		} catch (err) {
			addSystemMessage(`取消重试失败: ${err instanceof Error ? err.message : String(err)}`);
		}
	}, [addSystemMessage]);

	const toggleToolsExpanded = useCallback(() => {
		setToolsExpanded((prev) => !prev);
	}, []);

	const toggleShowThinkingAndTools = useCallback(() => {
		setShowThinkingAndTools((prev) => {
			const next = !prev;
			try {
				localStorage.setItem(SHOW_THINKING_TOOLS_KEY, String(next));
			} catch {
				/* ignore */
			}
			return next;
		});
	}, []);

	const respondExtensionDialog = useCallback((response: Record<string, unknown>) => {
		if (!extensionDialog) return;
		void chatApi.sendExtensionUiResponse(extensionDialog.id, response).catch(() => {});
		setExtensionDialog(null);
	}, [extensionDialog]);

	const dismissExtensionDialog = useCallback(() => {
		if (!extensionDialog) return;
		void chatApi
			.sendExtensionUiResponse(extensionDialog.id, { cancelled: true })
			.catch(() => {});
		setExtensionDialog(null);
	}, [extensionDialog]);

	const handleExtensionUiRequest = useCallback(
		(ev: Record<string, unknown>) => {
			const method = String(ev.method || "");
			if (method === "notify") {
				const notifyType = ev.notifyType === "error" ? "error" : ev.notifyType === "warning" ? "warning" : "info";
				const message = String(ev.message || "");
				if (!message || notifyType === "info") return;
				addSystemMessage(`[${notifyType}] ${message}`);
				return;
			}
			if (method === "setStatus") {
				const key = String(ev.statusKey || "status");
				const text = ev.statusText != null ? stripAnsi(String(ev.statusText)) : "";
				setExtensionStatuses((prev) => {
					const next = { ...prev };
					if (!text) delete next[key];
					else next[key] = text;
					return next;
				});
				return;
			}
			if (method === "setWidget") {
				const key = String(ev.widgetKey || "widget");
				const lines = Array.isArray(ev.widgetLines)
					? ev.widgetLines
							.filter((line): line is string => typeof line === "string")
							.map((line) => stripAnsi(line))
					: undefined;
				setExtensionWidgets((prev) => {
					const next = { ...prev };
					if (!lines?.length) delete next[key];
					else next[key] = lines;
					return next;
				});
				return;
			}
			if (method === "select" || method === "confirm" || method === "input" || method === "editor") {
				setExtensionDialog({
					id: String(ev.id || ""),
					method,
					title: String(ev.title || method),
					message: typeof ev.message === "string" ? ev.message : undefined,
					options: Array.isArray(ev.options)
						? ev.options.filter((option): option is string => typeof option === "string")
						: undefined,
					prefill: typeof ev.prefill === "string" ? ev.prefill : undefined,
				});
			}
		},
		[addSystemMessage],
	);

	const applyModelSelection = useCallback(
		async (key: string) => {
			if (!key || isApplyingModelSettings) return;
			const [provider, ...idParts] = key.split("/");
			const modelId = idParts.join("/");
			if (!provider || !modelId) return;

			setIsApplyingModelSettings(true);
			setHeaderMeta("正在切换模型");
			try {
				await chatApi.setModel(provider, modelId);
				await fetchSessionStateFull();
				setHeaderMeta("模型已切换");
				setTimeout(() => setHeaderMeta(""), 1000);
			} catch (err) {
				setHeaderMeta("");
				addSystemMessage(`切换模型失败: ${err instanceof Error ? err.message : String(err)}`);
				await fetchSessionStateFull();
			} finally {
				setIsApplyingModelSettings(false);
				await fetchSessionStateFull();
			}
		},
		[isApplyingModelSettings, addSystemMessage, fetchSessionStateFull],
	);

	const applyThinkingSelection = useCallback(
		async (level: string) => {
			if (isApplyingModelSettings) return;
			setIsApplyingModelSettings(true);
			setHeaderMeta("正在设置推理强度");
			try {
				await chatApi.setThinkingLevel(level);
				await fetchSessionStateFull();
				setHeaderMeta("推理强度已设置");
				setTimeout(() => setHeaderMeta(""), 1000);
			} catch (err) {
				setHeaderMeta("");
				addSystemMessage(`设置推理强度失败: ${err instanceof Error ? err.message : String(err)}`);
				await fetchSessionStateFull();
			} finally {
				setIsApplyingModelSettings(false);
				await fetchSessionStateFull();
			}
		},
		[isApplyingModelSettings, addSystemMessage, fetchSessionStateFull],
	);

	const processStreamEvent = useCallback(
		(ev: SseEvent) => {
			if (
				suppressStreamMessagesRef.current &&
				(ev.type === "message_start" ||
					ev.type === "message_update" ||
					ev.type === "message_end" ||
					ev.type === "tool_execution_start" ||
					ev.type === "tool_execution_update" ||
					ev.type === "tool_execution_end")
			) {
				return;
			}

			switch (ev.type) {
				case "agent_start":
					setIsStreaming(true);
					setConnected(true);
					streamingAssistantId.current = null;
					updateRunPhase("starting");
					scheduleSessionDashboardRefresh();
					break;

				case "agent_end":
					flushPendingStreamUpdates();
					setIsStreaming(false);
					setConnected(true);
					setMessages((prev) => finalizeStreamingTurn(prev));
					streamingAssistantId.current = null;
					if (backendSessionPathRef.current) {
						sessionCache.current.delete(backendSessionPathRef.current);
					}
					resetStreamingState();
					setTimeout(() => void loadSessions(), 1000);
					void fetchSessionStateFull();
					break;

				case "message_start": {
					const m = ev.message;
					if (m.role === "user") {
						const userText = extractContent(m.content);
						const userImages = extractImages(m.content);
						if (optimisticUserIdRef.current) {
							const optId = optimisticUserIdRef.current;
							optimisticUserIdRef.current = null;
							setMessages((prev) =>
								prev.map((msg) =>
									msg.id === optId
										? {
												...msg,
												content: userText || msg.content,
												images: userImages.length ? userImages : msg.images,
											}
										: msg,
								),
							);
							break;
						}
						setMessages((prev) => [
							...prev,
							{
								id: nextMessageId(),
								role: "user",
								content: userText,
								...(userImages.length ? { images: userImages } : {}),
							},
						]);
					}
					break;
				}

				case "message_update": {
					const m = ev.message;
					if (m.role === "assistant") {
						const blocks = extractAssistantBlocks(m.content);
						if (hasThinkingBlock(blocks)) {
							updateRunPhase("thinking");
						} else if (hasTextBlock(blocks)) {
							updateRunPhase("writing");
						}
						scheduleAssistantMessageUpdate(blocks);
					}
					break;
				}

				case "message_end": {
					flushPendingStreamUpdates();
					const m = ev.message;
					if (m.role === "assistant") {
						const blocks = extractAssistantBlocks(m.content);
						const toolCalls = getToolCalls(m.content);
						if (toolCalls.length > 0) {
							updateRunPhase("processing");
						} else if (hasThinkingBlock(blocks) && !hasTextBlock(blocks)) {
							updateRunPhase("thinking");
						} else if (hasTextBlock(blocks)) {
							updateRunPhase("writing");
						}
						setMessages((prev) =>
							finalizeAssistantMessageEnd(
								prev,
								blocks,
								toolCalls,
								resolveTurnAssistantId(prev, streamingAssistantId.current),
							),
						);
						streamingAssistantId.current = null;
						scheduleSessionDashboardRefresh();
					}
					break;
				}

				case "tool_execution_start": {
					const tid = ev.toolCallId ? String(ev.toolCallId) : undefined;
					updateRunPhase("tool", ev.toolName);
					const label = `🔧 ${ev.toolName}(${JSON.stringify(ev.args ?? {})})`;
					setMessages(
						(prev) => {
							const existingIdx = tid
								? prev.findIndex((msg) => msg.toolCallId === tid)
								: -1;
							if (existingIdx >= 0) {
								return prev.map((msg, i) =>
									i === existingIdx ? { ...msg, content: label } : msg,
								);
							}
							return [
								...prev,
								{
									id: tid ? `tool-${tid}` : nextMessageId(),
									role: "tool_call" as const,
									content: label,
									toolCallId: tid,
								},
							];
						},
						{ skipNormalize: true },
					);
					break;
				}

				case "tool_execution_update":
				case "tool_execution_end": {
					updateRunPhase("tool", ev.toolName);
					const tid = ev.toolCallId ? String(ev.toolCallId) : undefined;
					const isError = ev.type === "tool_execution_end" ? ev.isError : undefined;
					const full = buildToolCallUpdateText(ev, isError);
					setMessages(
						(prev) => {
							let idx = tid ? prev.findIndex((msg) => msg.toolCallId === tid) : -1;
							if (idx < 0) {
								const toolCalls = prev.filter((m) => m.role === "tool_call");
								if (toolCalls.length > 0) {
									idx = prev.findIndex((m) => m.id === toolCalls[toolCalls.length - 1].id);
								}
							}
							if (idx < 0) return prev;
							return prev.map((msg, i) => (i === idx ? { ...msg, content: full } : msg));
						},
						{ skipNormalize: true },
					);
					break;
				}

				case "compaction_start":
					setIsCompacting(true);
					break;

				case "compaction_end": {
					setIsCompacting(false);
					if (ev.aborted) {
						if (ev.errorMessage) addSystemMessage(ev.errorMessage);
						break;
					}
					const activePath = isEphemeralRef.current
						? backendSessionPathRef.current
						: activeSessionPathRef.current;
					const summaryText = ev.result?.summary?.trim();
					if (summaryText) {
						setMessages((prev) => [
							...prev,
							{
								id: nextMessageId(),
								role: "compaction_summary" as const,
								content: summaryText,
							},
						]);
					} else if (activePath && !compactionReloadPendingRef.current) {
						sessionCache.current.delete(activePath);
						void loadSession(activePath);
					}
					if (ev.willRetry) {
						addSystemMessage("压缩完成，正在重试当前请求…");
					}
					if (ev.errorMessage) {
						addSystemMessage(ev.errorMessage);
					} else if (ev.result?.tokensBefore) {
						addSystemMessage(
							`上下文已压缩（约 ${Math.round(ev.result.tokensBefore / 1000)}k tokens）`,
						);
					}
					break;
				}

				case "queue_update":
					setPendingSteering(Array.isArray(ev.steering) ? ev.steering.map(String) : []);
					setPendingFollowUp(Array.isArray(ev.followUp) ? ev.followUp.map(String) : []);
					break;

				case "auto_retry_start":
					setRetryState({
						active: true,
						attempt: ev.attempt,
						maxAttempts: ev.maxAttempts,
						delayMs: ev.delayMs,
						startedAt: Date.now(),
						errorMessage: ev.errorMessage,
					});
					break;

				case "auto_retry_end":
					setRetryState(null);
					if (ev.success === false && ev.finalError) {
						addSystemMessage(`自动重试失败: ${ev.finalError}`);
					}
					break;

				case "extension_error":
					if (!isIgnorableExtensionError(ev.error)) {
						addSystemMessage(
							`扩展错误${ev.extensionPath ? ` (${ev.extensionPath})` : ""}: ${ev.error || "unknown"}`,
						);
					}
					break;

				case "bash_update": {
					updateRunPhase("bash");
					const bashId = activeBashMsgIdRef.current;
					if (!bashId) break;
					const output = typeof ev.partialOutput === "string" ? ev.partialOutput : "";
					scheduleBashOutputUpdate(output);
					break;
				}
			}
		},
		[
			scheduleSessionDashboardRefresh,
			loadSessions,
			loadSession,
			fetchSessionStateFull,
			addSystemMessage,
			resetStreamingState,
			updateRunPhase,
			scheduleAssistantMessageUpdate,
			scheduleBashOutputUpdate,
			flushPendingStreamUpdates,
		],
	);

	const handleSseEvent = useCallback(
		(ev: SseEvent) => {
			switch (ev.type) {
				case "connected":
					setConnected(true);
					if (ev.isStreaming !== undefined) {
						setIsStreaming(ev.isStreaming);
					}
					for (const replayEv of ev.replay ?? []) {
						if (suppressStreamMessagesRef.current) {
							if (
								replayEv.type === "message_start" ||
								replayEv.type === "message_update" ||
								replayEv.type === "message_end" ||
								replayEv.type === "tool_execution_start" ||
								replayEv.type === "tool_execution_update" ||
								replayEv.type === "tool_execution_end"
							) {
								continue;
							}
						}
						if (replayEv.type === "extension_ui_request") {
							handleExtensionUiRequest(replayEv as Record<string, unknown>);
						} else {
							processStreamEvent(replayEv);
						}
					}
					void fetchSessionStateFull();
					break;

				case "prompt_rejected": {
					invalidateTurnEvents();
					const failedId = optimisticUserIdRef.current;
					optimisticUserIdRef.current = null;
					if (failedId) {
						setMessages((prev) => prev.filter((msg) => msg.id !== failedId));
					}
					addSystemMessage(`发送失败: ${ev.error}`);
					break;
				}

				case "extension_ui_request":
					handleExtensionUiRequest(ev as Record<string, unknown>);
					break;

				default:
					processStreamEvent(ev);
			}
		},
		[
			processStreamEvent,
			fetchSessionStateFull,
			invalidateTurnEvents,
			addSystemMessage,
			handleExtensionUiRequest,
		],
	);

	const handleSseEventRef = useRef(handleSseEvent);
	const addSystemMessageRef = useRef(addSystemMessage);
	const restoreSessionOnBootRef = useRef(restoreSessionOnBoot);
	handleSseEventRef.current = handleSseEvent;
	addSystemMessageRef.current = addSystemMessage;
	restoreSessionOnBootRef.current = restoreSessionOnBoot;

	useEffect(() => {
		syncViewportHeight();
		const onResize = () => {
			syncViewportHeight();
			if (window.innerWidth > 768) setSidebarOpen(false);
		};
		window.addEventListener("resize", onResize);
		window.visualViewport?.addEventListener("resize", syncViewportHeight);
		window.visualViewport?.addEventListener("scroll", syncViewportHeight);
		window.addEventListener("orientationchange", syncViewportHeight);

		let cancelled = false;

		void restoreSessionOnBootRef.current().catch(() => {});

		void chatApi
			.fetchModels()
			.then((data) => {
				if (!cancelled) setAvailableModels(data.models || []);
			})
			.catch((err) => {
				if (!cancelled) {
					addSystemMessageRef.current(
						`模型列表加载失败: ${err instanceof Error ? err.message : String(err)}`,
					);
				}
			});

		let eventSource: EventSource | null = null;
		let reconnectTimer: ReturnType<typeof setTimeout> | null = null;

		const connect = () => {
			if (eventSource) eventSource.close();
			eventSource = new EventSource(apiUrl("/api/events"));
			eventSource.onopen = () => {
				setConnected(true);
			};
			eventSource.onmessage = (e) => {
				try {
					handleSseEventRef.current(JSON.parse(e.data) as SseEvent);
				} catch {
					/* ignore */
				}
			};
			eventSource.onerror = () => {
				setConnected(false);
				if (reconnectTimer) clearTimeout(reconnectTimer);
				reconnectTimer = setTimeout(connect, 2000);
			};
		};

		connect();

		return () => {
			cancelled = true;
			window.removeEventListener("resize", onResize);
			window.visualViewport?.removeEventListener("resize", syncViewportHeight);
			window.visualViewport?.removeEventListener("scroll", syncViewportHeight);
			window.removeEventListener("orientationchange", syncViewportHeight);
			if (reconnectTimer) clearTimeout(reconnectTimer);
			eventSource?.close();
		};
	}, []);

	const toggleSidebar = useCallback(() => setSidebarOpen((o) => !o), []);
	const closeSidebar = useCallback(() => setSidebarOpen(false), []);

	const value = useMemo<ChatContextValue>(
		() => ({
			connected,
			isStreaming,
			messages,
			sessions,
			activeSessionPath,
			headerTitle,
			headerMeta,
			sidebarOpen,
			sessionState,
			availableModels,
			currentModelKey,
			thinkingLevel,
			availableThinkingLevels,
			isApplyingModelSettings,
			pendingSteering,
			pendingFollowUp,
			retryState,
			isCompacting,
			runPhase,
			extensionWidgets,
			extensionStatuses,
			extensionDialog,
			toolsExpanded,
			showThinkingAndTools,
			toggleSidebar,
			closeSidebar,
			loadSessions,
			loadSession,
			deleteSession: deleteSessionHandler,
			renameSession: renameSessionHandler,
			toggleSessionPin: toggleSessionPinHandler,
			newSession,
			newTemporarySession,
			isEphemeralSession,
			sendMessage,
			runBashCommand,
			abortStream,
			abortRetry,
			toggleToolsExpanded,
			toggleShowThinkingAndTools,
			respondExtensionDialog,
			dismissExtensionDialog,
			applyModelSelection,
			applyThinkingSelection,
			addSystemMessage,
		}),
		[
			connected,
			isStreaming,
			messages,
			sessions,
			activeSessionPath,
			headerTitle,
			headerMeta,
			sidebarOpen,
			sessionState,
			availableModels,
			currentModelKey,
			thinkingLevel,
			availableThinkingLevels,
			isApplyingModelSettings,
			pendingSteering,
			pendingFollowUp,
			retryState,
			isCompacting,
			runPhase,
			extensionWidgets,
			extensionStatuses,
			extensionDialog,
			toolsExpanded,
			showThinkingAndTools,
			toggleSidebar,
			closeSidebar,
			loadSessions,
			loadSession,
			deleteSessionHandler,
			renameSessionHandler,
			toggleSessionPinHandler,
			newSession,
			newTemporarySession,
			isEphemeralSession,
			sendMessage,
			runBashCommand,
			abortStream,
			abortRetry,
			toggleToolsExpanded,
			toggleShowThinkingAndTools,
			respondExtensionDialog,
			dismissExtensionDialog,
			applyModelSelection,
			applyThinkingSelection,
			addSystemMessage,
		],
	);

	return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
}

export function useChatContext(): ChatContextValue {
	const ctx = useContext(ChatContext);
	if (!ctx) throw new Error("useChatContext must be used within ChatProvider");
	return ctx;
}
