ChatVeil

Mask non-current chatbot conversation titles with hover reveal and per-title unlocks.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         ChatVeil
// @namespace    https://github.com/local/chatveil
// @version      0.1.0
// @description  Mask non-current chatbot conversation titles with hover reveal and per-title unlocks.
// @author       Wonster
// @license      MIT
// @match        https://chatgpt.com/*
// @match        https://*.chatgpt.com/*
// @match        https://claude.ai/*
// @match        https://*.claude.ai/*
// @match        https://qwen.ai/*
// @match        https://*.qwen.ai/*
// @match        https://chat.qwen.ai/*
// @match        https://minimax.io/*
// @match        https://*.minimax.io/*
// @match        https://agent.minimax.com/*
// @match        https://agent.minimaxi.com/*
// @match        https://hailuo.ai/*
// @match        https://*.hailuo.ai/*
// @match        https://hailuoai.video/*
// @match        https://*.hailuoai.video/*
// @match        https://chat.deepseek.com/*
// @match        https://kimi.moonshot.cn/*
// @match        https://doubao.com/*
// @match        https://*.doubao.com/*
// @match        https://yuanbao.tencent.com/*
// @match        https://tongyi.com/*
// @match        https://*.tongyi.com/*
// @match        https://tongyi.aliyun.com/*
// @match        https://yiyan.baidu.com/*
// @match        https://gemini.google.com/*
// @match        https://copilot.microsoft.com/*
// @match        https://perplexity.ai/*
// @match        https://*.perplexity.ai/*
// @match        https://poe.com/*
// @match        https://*.poe.com/*
// @match        https://chat.mistral.ai/*
// @match        https://grok.com/*
// @match        https://*.grok.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==
(function() {
	//#region src/core/constants.ts
	var SETTINGS_KEY = "chatVeilSettings";
	var DATA_PREFIX = "chatveil";
	var STYLE_ID = "chatveil-mask-style";
	var KNOWN_HOSTS = [
		"chatgpt.com",
		"claude.ai",
		"qwen.ai",
		"chat.qwen.ai",
		"minimax.io",
		"chat.minimax.io",
		"agent.minimax.com",
		"agent.minimaxi.com",
		"hailuo.ai",
		"hailuoai.video",
		"chat.deepseek.com",
		"kimi.moonshot.cn",
		"doubao.com",
		"www.doubao.com",
		"yuanbao.tencent.com",
		"tongyi.com",
		"www.tongyi.com",
		"tongyi.aliyun.com",
		"yiyan.baidu.com",
		"gemini.google.com",
		"copilot.microsoft.com",
		"perplexity.ai",
		"poe.com",
		"chat.mistral.ai",
		"grok.com"
	];
	var DEFAULT_SETTINGS = {
		version: 1,
		enabledHosts: {},
		customHosts: [],
		maskStyle: "skeleton",
		customMaskChar: "•",
		showAllUntilByHost: {},
		unlockedKeysByHost: {},
		syncEnabled: false
	};
	Array.from(new Set(KNOWN_HOSTS.flatMap((host) => [`https://${host}/*`, `https://*.${host}/*`])));
	//#endregion
	//#region src/core/adapters.ts
	var titleSelector = [
		"[data-testid*=\"conversation\"]",
		"[data-testid*=\"history\"]",
		"[data-testid*=\"chat\"]",
		"[title]",
		"span",
		"p",
		"div"
	].join(",");
	var rowActionSelector = [
		"button",
		"[role=\"button\"]",
		"[aria-haspopup=\"menu\"]",
		"[aria-label*=\"menu\" i]",
		"[aria-label*=\"more\" i]",
		"[aria-label*=\"option\" i]",
		"[aria-label*=\"action\" i]",
		"[aria-label*=\"conversation\" i]",
		"[aria-label*=\"chat\" i]",
		"[aria-label*=\"更多\" i]",
		"[data-testid*=\"menu\" i]",
		"[data-testid*=\"more\" i]",
		"[data-testid*=\"option\" i]",
		"[data-testid*=\"action\" i]"
	].join(",");
	function normalizedText(element) {
		return element?.textContent?.replace(/\s+/g, " ").trim() ?? "";
	}
	function normalizeComparableText(text) {
		return text.replace(/\s+/g, "").trim().toLowerCase();
	}
	function shouldIgnoreTitleText(text) {
		const normalized = normalizeComparableText(text);
		if (!normalized) return true;
		if (/^(19|20)\d{2}([-/.]?\d{1,2})?$/.test(normalized)) return true;
		return [
			"newchat",
			"newconversation",
			"newthread",
			"settings",
			"logout",
			"login",
			"upgrade",
			"help",
			"search",
			"newtask",
			"explore",
			"discover",
			"apps",
			"tools",
			"plugins",
			"community",
			"chats",
			"projects",
			"newproject",
			"artifacts",
			"code",
			"coder",
			"customize",
			"recents",
			"today",
			"yesterday",
			"allchats",
			"allconversations",
			"slides",
			"docs",
			"deepresearch",
			"websites",
			"sheets",
			"agentswarm",
			"kimicode",
			"kimiclaw",
			"chathistory",
			"mimoclaw",
			"mimochat",
			"maxhermes",
			"maxclaw",
			"skills",
			"新对话",
			"新建对话",
			"开启新对话",
			"新建任务",
			"ai创作",
			"更多",
			"设置",
			"登录",
			"退出登录",
			"帮助",
			"搜索",
			"发现",
			"探索",
			"应用",
			"插件",
			"工具",
			"社区",
			"项目",
			"新项目",
			"所有对话",
			"今天",
			"昨天",
			"资产",
			"专家",
			"探索专家",
			"任务记录",
			"新会话",
			"历史",
			"历史记录",
			"skills技能",
			"minimax实验室"
		].includes(normalized);
	}
	function cleanPath(pathname) {
		return pathname.replace(/\/+$/g, "") || "/";
	}
	function parseHref(href, base) {
		if (!href) return void 0;
		try {
			return new URL(href, base);
		} catch {
			return;
		}
	}
	function hostMatches(hostname, host) {
		const clean = hostname.toLowerCase();
		return clean === host || clean.endsWith(`.${host}`);
	}
	function isElement(value) {
		return value !== null && typeof value === "object" && "nodeType" in value && value.nodeType === 1;
	}
	function closestLinkActionRow(link) {
		let current = link.parentElement;
		for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
			if (current === current.ownerDocument.body || current === current.ownerDocument.documentElement) break;
			const textLength = normalizedText(current).length;
			if (textLength < 2 || textLength > 260) continue;
			if (current.querySelectorAll("a[href], [role=\"link\"]").length !== 1) continue;
			if (!current.querySelector(rowActionSelector)) continue;
			return current;
		}
	}
	function closestRow(element) {
		const link = element.closest("a[href], [role=\"link\"]") ?? element;
		const actionRow = closestLinkActionRow(link);
		if (actionRow) return actionRow;
		const container = Array.from(link.closest("body")?.querySelectorAll("li, [role=\"treeitem\"], [role=\"listitem\"], [data-testid], [data-qa], [aria-label*=\"conversation\" i], [aria-label*=\"chat\" i], [class*=\"conversation\"], [class*=\"history-item\"], [class*=\"chat-item\"], [class*=\"thread-item\"], [class*=\"session-item\"]") ?? []).filter((container) => container.contains(link)).sort((left, right) => {
			return normalizedText(left).length - normalizedText(right).length;
		}).find((candidate) => {
			const textLength = normalizedText(candidate).length;
			return textLength >= 2 && textLength <= 260 && Boolean(candidate.querySelector("a[href], [role=\"link\"], button, [role=\"button\"]"));
		}) ?? link.closest("li, [role=\"treeitem\"], [role=\"listitem\"], [data-testid], [data-qa]");
		return container?.querySelector("a[href], [role=\"link\"], button, [role=\"button\"]") ? container : link;
	}
	function chooseTitleElement(anchor) {
		const titleAttrElement = anchor.querySelector("[title]");
		if (titleAttrElement && normalizedText(titleAttrElement).length > 0 && !hasNonTitleChild(titleAttrElement)) return titleAttrElement;
		const candidate = Array.from(anchor.querySelectorAll(titleSelector)).filter((candidate) => {
			const controlAncestor = candidate.closest("button, [role=\"button\"]");
			if (controlAncestor && controlAncestor !== anchor) return false;
			if (candidate.closest("svg")) return false;
			if (isActionElement(candidate)) return false;
			if (isMediaElement(candidate)) return false;
			if (hasNonTitleChild(candidate)) return false;
			const text = normalizedText(candidate);
			return text.length >= 2 && text.length <= 180;
		}).sort((left, right) => {
			const leftText = normalizedText(left);
			const rightText = normalizedText(right);
			return leftText.length - rightText.length;
		})[0];
		if (candidate) return candidate;
		return candidate ?? ensureTextOnlyElement(anchor);
	}
	function isMediaElement(element) {
		const className = typeof element.className === "string" ? element.className : "";
		return [
			"IMG",
			"SVG",
			"CANVAS",
			"PICTURE",
			"VIDEO"
		].includes(element.tagName) || element.getAttribute("role") === "img" || /\b(icon|avatar)\b/i.test(className);
	}
	function hasMediaChild(element) {
		return Boolean(element.querySelector("img, svg, canvas, picture, video, [role=\"img\"], .icon, [class*=\"icon\"], [class*=\"avatar\"]"));
	}
	function isActionElement(element) {
		return element.matches(rowActionSelector);
	}
	function hasActionChild(element) {
		return Boolean(element.querySelector(rowActionSelector));
	}
	function hasNonTitleChild(element) {
		return hasMediaChild(element) || hasActionChild(element);
	}
	function isProfileOrAccountRow(row) {
		const marker = [
			row.className,
			row.id,
			row.getAttribute("data-testid"),
			row.getAttribute("data-qa"),
			row.getAttribute("aria-label")
		].join(" ").toLowerCase();
		const text = normalizedText(row).toLowerCase();
		return /\b(user|profile|account|avatar|workspace|sidebar-user|footer)\b/.test(marker) || Boolean(row.closest("footer, [class*=\"footer\" i], [class*=\"account\" i], [class*=\"profile\" i], [class*=\"user\" i]")) || hasMediaChild(row) && /\b(pro|free|team|account|profile|个人|账号)\b/i.test(text);
	}
	function ensureTextOnlyElement(row, forceWrapper = false) {
		const textNode = row.ownerDocument.createTreeWalker(row, NodeFilter.SHOW_TEXT, { acceptNode(node) {
			const text = node.textContent?.replace(/\s+/g, " ").trim() ?? "";
			if (text.length < 2 || text.length > 180) return NodeFilter.FILTER_REJECT;
			const parent = node.parentElement;
			if (!parent) return NodeFilter.FILTER_REJECT;
			const controlAncestor = parent.closest("button, [role=\"button\"]");
			if (!forceWrapper && controlAncestor && controlAncestor !== row) return NodeFilter.FILTER_REJECT;
			if (parent.closest("svg, [aria-hidden=\"true\"], [role=\"img\"], script, style")) return NodeFilter.FILTER_REJECT;
			if (isActionElement(parent)) return NodeFilter.FILTER_REJECT;
			if (hasNonTitleChild(parent) && normalizedText(parent) === text) return NodeFilter.FILTER_REJECT;
			return NodeFilter.FILTER_ACCEPT;
		} }).nextNode();
		if (!textNode?.parentNode) return row;
		const parent = textNode.parentElement;
		if (!forceWrapper && parent && parent.childNodes.length === 1 && !hasNonTitleChild(parent)) return parent;
		const wrapper = row.ownerDocument.createElement("span");
		wrapper.dataset.chatveilTextWrapper = "true";
		textNode.parentNode.insertBefore(wrapper, textNode);
		wrapper.append(textNode);
		return wrapper;
	}
	function hasActiveMarker(element) {
		const marker = [
			element.getAttribute("aria-current"),
			element.getAttribute("data-active"),
			element.getAttribute("data-current"),
			element.getAttribute("data-selected"),
			element.className
		].join(" ").toLowerCase();
		return /\b(page|true|active|current|selected)\b/.test(marker);
	}
	function isCurrentByHref(item, location) {
		if (hasActiveMarker(item.row) || hasActiveMarker(item.title)) return true;
		const url = parseHref(item.href, location.href);
		if (!url || url.origin !== location.origin) return false;
		return cleanPath(url.pathname) === cleanPath(location.pathname);
	}
	function stableKeyFromHref(item, location, index) {
		const href = parseHref(item.href, location.href);
		if (href) return `${href.origin}${cleanPath(href.pathname)}`;
		const text = normalizedText(item.title).toLowerCase();
		return `${location.origin}${cleanPath(location.pathname)}#${item.source ?? "item"}:${index}:${text}`;
	}
	function stableKeyFromDeepSeek(item, location, index) {
		const id = item.row.getAttribute("data-conversation-id") ?? item.row.getAttribute("data-chat-id") ?? item.row.getAttribute("data-session-id") ?? item.row.getAttribute("data-id");
		if (id) return `${location.origin}/deepseek/${id}`;
		return stableKeyFromHref(item, location, index);
	}
	function itemsFromAnchors(root, predicate, source) {
		return itemsFromAnchorsInContainers([root], root, predicate, source);
	}
	function itemsFromAnchorsInContainers(containers, root, predicate, source) {
		const base = root.ownerDocument?.baseURI ?? globalThis.location?.href ?? "https://example.test/";
		const seenRows = /* @__PURE__ */ new Set();
		const seenAnchors = /* @__PURE__ */ new Set();
		const output = [];
		const anchors = containers.flatMap((container) => Array.from(container.querySelectorAll("a[href]")));
		for (const anchor of anchors) {
			if (seenAnchors.has(anchor)) continue;
			seenAnchors.add(anchor);
			const url = parseHref(anchor.getAttribute("href") ?? anchor.href, base);
			if (!url || !predicate(url, anchor)) continue;
			const title = chooseTitleElement(anchor);
			const text = normalizedText(title);
			if (text.length < 2 || text.length > 180) continue;
			if (shouldIgnoreTitleText(text)) continue;
			const row = closestRow(anchor);
			if (seenRows.has(row)) continue;
			seenRows.add(row);
			output.push({
				row,
				title,
				href: url.href,
				source
			});
		}
		return output;
	}
	function itemFromGenericLink(anchor, source) {
		const title = chooseTitleElement(anchor);
		const text = normalizedText(title);
		if (text.length < 2 || text.length > 180) return void 0;
		if (shouldIgnoreTitleText(text)) return void 0;
		return {
			row: closestRow(anchor),
			title,
			href: anchor.href || anchor.getAttribute("href") || void 0,
			source
		};
	}
	function isLikelyChatHistoryUrl(url) {
		return /^\/(a\/)?chat(\/s)?\/[^/]+/.test(url.pathname) || /^\/(c|conversation|thread|session|chatroom)\/[^/]+/.test(url.pathname);
	}
	function historyContainers(root) {
		return Array.from(root.querySelectorAll("aside, nav, [role=\"navigation\"], [data-sidebar], [class*=\"sidebar\"], [class*=\"history\"], [class*=\"conversation\"], [class*=\"chat-list\"], [class*=\"thread-list\"], [class*=\"session-list\"], [class*=\"task-list\"], [class*=\"record-list\"]"));
	}
	function isSidebarLikeContainer(element) {
		return Boolean(element.closest("aside, nav, [role=\"navigation\"], [data-sidebar], [class*=\"sidebar\"]"));
	}
	function sidebarHistoryContainers(root) {
		return historyContainers(root).filter(isSidebarLikeContainer);
	}
	function isLeftSidebarRow(element) {
		const rect = element.getBoundingClientRect();
		if (rect.width <= 0 && rect.height <= 0) return true;
		const viewportWidth = element.ownerDocument.defaultView?.innerWidth ?? 0;
		if (viewportWidth <= 0) return true;
		const maxRight = Math.min(760, viewportWidth * .5);
		const maxWidth = Math.min(620, viewportWidth * .45);
		return rect.left >= -8 && rect.left <= Math.min(520, viewportWidth * .4) && rect.right <= maxRight && rect.width <= maxWidth;
	}
	function itemsFromSidebarAnchors(root, predicate, source) {
		return itemsFromAnchorsInContainers(sidebarHistoryContainers(root), root, predicate, source);
	}
	function itemFromClickableRow(row, source) {
		const title = chooseTitleElement(row);
		const text = normalizedText(title);
		if (text.length < 2 || text.length > 180) return void 0;
		if (shouldIgnoreTitleText(text)) return void 0;
		return {
			row,
			title,
			href: row.querySelector("a[href]")?.href ?? void 0,
			source
		};
	}
	function itemsFromClickableRows(root, source) {
		const seenRows = /* @__PURE__ */ new Set();
		const output = [];
		for (const container of historyContainers(root)) {
			if (!isElement(container)) continue;
			const candidates = Array.from(container.querySelectorAll([
				"[data-conversation-id]",
				"[data-chat-id]",
				"[data-session-id]",
				"[class*=\"conversation-item\"]",
				"[class*=\"history-item\"]",
				"[class*=\"chat-item\"]",
				"[class*=\"thread-item\"]",
				"[class*=\"session-item\"]",
				"li[role=\"button\"]",
				"[role=\"listitem\"][tabindex]",
				"div[tabindex]"
			].join(",")));
			for (const row of candidates) {
				if (row.querySelector("input, textarea")) continue;
				const className = typeof row.className === "string" ? row.className : "";
				if (!(row.hasAttribute("data-conversation-id") || row.hasAttribute("data-chat-id") || row.hasAttribute("data-session-id") || /conversation|history|chat-item|thread|session|task|record/i.test(className))) continue;
				const item = itemFromClickableRow(row, source);
				if (!item || seenRows.has(item.row)) continue;
				seenRows.add(item.row);
				output.push(item);
			}
		}
		return output;
	}
	function isLikelyPlainHistoryRow(row, text) {
		if (row.querySelector("input, textarea, select")) return false;
		if (isProfileOrAccountRow(row)) return false;
		if (row.matches("aside, nav, section, main, header, footer")) return false;
		if (row.querySelectorAll("a[href], button, [role=\"button\"]").length > 1) return false;
		if (/^(history|recents|chat history|all chats|all conversations|today|yesterday|任务记录|历史|历史记录|所有对话|今天|昨天)$/i.test(text)) return false;
		if (/^(19|20)\d{2}([-/.]\d{1,2})?$/.test(text.trim())) return false;
		if (Array.from(row.children).filter((child) => child instanceof HTMLElement).filter((child) => !isActionElement(child) && !isMediaElement(child) && !hasMediaChild(child)).map((child) => normalizedText(child)).filter((childText) => childText.length >= 2).length >= 2) return false;
		return true;
	}
	function itemsFromLabeledPlainRows(root, source, labelPattern) {
		const output = [];
		const seenTexts = /* @__PURE__ */ new Set();
		const containers = sidebarHistoryContainers(root);
		for (const container of containers) {
			const candidates = Array.from(container.querySelectorAll("button, div, li, [role=\"button\"], [role=\"listitem\"], [data-testid], [data-qa], [class*=\"item\"], [class*=\"row\"], [class*=\"task\"], [class*=\"record\"]"));
			const label = candidates.find((candidate) => labelPattern.test(normalizedText(candidate)));
			if (!label) continue;
			if (containers.some((other) => other !== container && container.contains(other) && other.contains(label))) continue;
			for (const row of candidates) {
				if (!(label.compareDocumentPosition(row) & Node.DOCUMENT_POSITION_FOLLOWING)) continue;
				if (source === "minimax" && !isLeftSidebarRow(row)) continue;
				const text = normalizedText(row);
				if (text.length < 2 || text.length > 180) continue;
				if (shouldIgnoreTitleText(text)) continue;
				if (!isLikelyPlainHistoryRow(row, text)) continue;
				const key = normalizeComparableText(text);
				if (seenTexts.has(key)) continue;
				const item = itemFromClickableRow(row, source);
				if (!item) continue;
				seenTexts.add(key);
				output.push(item);
			}
		}
		return output;
	}
	var chatGptAdapter = {
		id: "chatgpt",
		name: "ChatGPT",
		matches(location) {
			return hostMatches(location.hostname, "chatgpt.com");
		},
		findHistoryItems(root) {
			return itemsFromAnchors(root, (url) => url.hostname.endsWith("chatgpt.com") && /^\/(c|g)\/[^/]+/.test(url.pathname), "chatgpt");
		},
		isCurrent: isCurrentByHref,
		getStableKey: stableKeyFromHref
	};
	var claudeAdapter = {
		id: "claude",
		name: "Claude",
		matches(location) {
			return hostMatches(location.hostname, "claude.ai");
		},
		findHistoryItems(root) {
			return itemsFromAnchors(root, (url) => url.hostname.endsWith("claude.ai") && /^\/chat\/[^/]+/.test(url.pathname), "claude");
		},
		isCurrent: isCurrentByHref,
		getStableKey: stableKeyFromHref
	};
	var qwenAdapter = {
		id: "qwen",
		name: "Qwen",
		matches(location) {
			return hostMatches(location.hostname, "qwen.ai");
		},
		findHistoryItems(root) {
			const anchorItems = itemsFromAnchors(root, (url) => url.hostname.endsWith("qwen.ai") && /^\/(chat|c)\/[^/]+/.test(url.pathname), "qwen");
			const seenRows = new Set(anchorItems.map((item) => item.row));
			const rowItems = itemsFromLabeledPlainRows(root, "qwen", /^(all chats|all conversations|today|yesterday|所有对话|今天|昨天|(19|20)\d{2}([-/.]\d{1,2})?)$/i).filter((item) => !seenRows.has(item.row));
			return [...anchorItems, ...rowItems];
		},
		isCurrent: isCurrentByHref,
		getStableKey: stableKeyFromHref
	};
	var deepSeekAdapter = {
		id: "deepseek",
		name: "DeepSeek",
		matches(location) {
			return hostMatches(location.hostname, "chat.deepseek.com");
		},
		findHistoryItems(root) {
			const anchorItems = itemsFromAnchors(root, (url) => url.hostname.endsWith("chat.deepseek.com") && isLikelyChatHistoryUrl(url), "deepseek");
			const seenRows = new Set(anchorItems.map((item) => item.row));
			const rowItems = itemsFromClickableRows(root, "deepseek").filter((item) => !seenRows.has(item.row));
			return [...anchorItems, ...rowItems];
		},
		isCurrent: isCurrentByHref,
		getStableKey: stableKeyFromDeepSeek
	};
	var minimaxAdapter = {
		id: "minimax",
		name: "MiniMax",
		matches(location) {
			return hostMatches(location.hostname, "minimax.io") || hostMatches(location.hostname, "agent.minimax.com") || hostMatches(location.hostname, "agent.minimaxi.com");
		},
		findHistoryItems(root) {
			const anchorItems = itemsFromSidebarAnchors(root, (url) => (url.hostname.endsWith("minimax.io") || url.hostname.endsWith("minimax.com") || url.hostname.endsWith("minimaxi.com")) && (/^\/(chat|conversation|thread|task)\b/.test(url.pathname) || url.searchParams.has("id")), "minimax").filter((item) => isLeftSidebarRow(item.row));
			const seenRows = new Set(anchorItems.map((item) => item.row));
			const rowItems = itemsFromLabeledPlainRows(root, "minimax", /^(任务记录|task records?\b|task history\b|history\b)/i).filter((item) => !seenRows.has(item.row));
			return [...anchorItems, ...rowItems];
		},
		isCurrent: isCurrentByHref,
		getStableKey: stableKeyFromHref
	};
	var genericSidebarAdapter = {
		id: "generic",
		name: "Generic chatbot sidebar",
		matches() {
			return true;
		},
		findHistoryItems(root) {
			const containers = historyContainers(root);
			const seenRows = /* @__PURE__ */ new Set();
			const output = [];
			for (const container of containers) {
				if (!isElement(container)) continue;
				for (const anchor of Array.from(container.querySelectorAll("a[href]"))) {
					const parsed = parseHref(anchor.getAttribute("href") ?? anchor.href, root.ownerDocument?.baseURI ?? globalThis.location?.href ?? "https://example.test/");
					if (parsed && !isLikelyChatHistoryUrl(parsed) && !/\/(chat|conversation|thread|session)\b/.test(parsed.pathname)) continue;
					const item = itemFromGenericLink(anchor, "generic");
					if (!item || seenRows.has(item.row)) continue;
					seenRows.add(item.row);
					output.push(item);
				}
			}
			for (const item of itemsFromClickableRows(root, "generic")) {
				if (seenRows.has(item.row)) continue;
				seenRows.add(item.row);
				output.push(item);
			}
			return output;
		},
		isCurrent: isCurrentByHref,
		getStableKey: stableKeyFromHref
	};
	var defaultAdapters = [
		chatGptAdapter,
		claudeAdapter,
		qwenAdapter,
		deepSeekAdapter,
		minimaxAdapter,
		genericSidebarAdapter
	];
	function selectAdapter(adapters, location) {
		return adapters.find((adapter) => adapter.id !== "generic" && adapter.matches(location)) ?? genericSidebarAdapter;
	}
	//#endregion
	//#region src/core/hash.ts
	var encoder = new TextEncoder();
	function toHex(buffer) {
		return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
	}
	function fallbackHash(input) {
		let hash = 2166136261;
		for (let index = 0; index < input.length; index += 1) {
			hash ^= input.charCodeAt(index);
			hash = Math.imul(hash, 16777619) >>> 0;
		}
		return hash.toString(16).padStart(8, "0");
	}
	async function hashStableKey(input) {
		const subtle = globalThis.crypto?.subtle;
		if (!subtle) return `sha256:${fallbackHash(input)}`;
		return `sha256:${toHex(await subtle.digest("SHA-256", encoder.encode(input)))}`;
	}
	//#endregion
	//#region src/core/settings.ts
	var maskStyles = [
		"skeleton",
		"characters",
		"blur"
	];
	function isRecord(value) {
		return Boolean(value) && typeof value === "object" && !Array.isArray(value);
	}
	function cleanHost(host) {
		return host.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
	}
	function isKnownHost(hostname) {
		const host = cleanHost(hostname);
		return KNOWN_HOSTS.some((knownHost) => host === knownHost || host.endsWith(`.${knownHost}`));
	}
	function normalizeHostList(value) {
		if (!Array.isArray(value)) return [];
		return Array.from(new Set(value.filter((host) => typeof host === "string").map(cleanHost).filter(Boolean))).sort();
	}
	function normalizeBooleanRecord(value) {
		if (!isRecord(value)) return {};
		const output = {};
		for (const [host, enabled] of Object.entries(value)) {
			const clean = cleanHost(host);
			if (!clean || typeof enabled !== "boolean") continue;
			output[clean] = enabled;
		}
		return output;
	}
	function normalizeNumberRecord(value) {
		if (!isRecord(value)) return {};
		const output = {};
		for (const [host, timestamp] of Object.entries(value)) {
			const clean = cleanHost(host);
			if (!clean || typeof timestamp !== "number" || !Number.isFinite(timestamp)) continue;
			output[clean] = Math.max(0, timestamp);
		}
		return output;
	}
	function normalizeStringArrayRecord(value) {
		if (!isRecord(value)) return {};
		const output = {};
		for (const [host, keys] of Object.entries(value)) {
			const clean = cleanHost(host);
			if (!clean || !Array.isArray(keys)) continue;
			output[clean] = Array.from(new Set(keys.filter((key) => typeof key === "string" && key.startsWith("sha256:")))).sort();
		}
		return output;
	}
	function normalizeSettings(value) {
		if (!isRecord(value)) return { ...DEFAULT_SETTINGS };
		const maskStyle = maskStyles.includes(value.maskStyle) ? value.maskStyle : DEFAULT_SETTINGS.maskStyle;
		const customMaskChar = typeof value.customMaskChar === "string" && value.customMaskChar.trim() ? Array.from(value.customMaskChar.trim()).slice(0, 120).join("") : DEFAULT_SETTINGS.customMaskChar;
		return {
			version: 1,
			enabledHosts: normalizeBooleanRecord(value.enabledHosts),
			customHosts: normalizeHostList(value.customHosts),
			maskStyle,
			customMaskChar,
			showAllUntilByHost: normalizeNumberRecord(value.showAllUntilByHost),
			unlockedKeysByHost: normalizeStringArrayRecord(value.unlockedKeysByHost),
			syncEnabled: typeof value.syncEnabled === "boolean" ? value.syncEnabled : DEFAULT_SETTINGS.syncEnabled
		};
	}
	function getSiteStatus(settings, hostname) {
		const host = cleanHost(hostname);
		const known = isKnownHost(host);
		const custom = settings.customHosts.includes(host);
		return {
			host,
			known,
			custom,
			enabled: settings.enabledHosts[host] ?? (known || custom)
		};
	}
	function canRunOnHost(settings, hostname) {
		return getSiteStatus(settings, hostname).enabled;
	}
	function updateHostEnabled(settings, hostname, enabled) {
		const host = cleanHost(hostname);
		return normalizeSettings({
			...settings,
			enabledHosts: {
				...settings.enabledHosts,
				[host]: enabled
			}
		});
	}
	function addCustomHost(settings, hostname) {
		const host = cleanHost(hostname);
		return normalizeSettings({
			...settings,
			customHosts: [...settings.customHosts, host],
			enabledHosts: {
				...settings.enabledHosts,
				[host]: true
			}
		});
	}
	function removeCustomHost(settings, hostname) {
		const host = cleanHost(hostname);
		const { [host]: _removedEnabled, ...enabledHosts } = settings.enabledHosts;
		const { [host]: _removedShowAll, ...showAllUntilByHost } = settings.showAllUntilByHost;
		const { [host]: _removedUnlocked, ...unlockedKeysByHost } = settings.unlockedKeysByHost;
		return normalizeSettings({
			...settings,
			customHosts: settings.customHosts.filter((customHost) => customHost !== host),
			enabledHosts,
			showAllUntilByHost,
			unlockedKeysByHost
		});
	}
	function setUnlockedKey(settings, hostname, key, unlocked) {
		const host = cleanHost(hostname);
		const current = new Set(settings.unlockedKeysByHost[host] ?? []);
		if (unlocked) current.add(key);
		else current.delete(key);
		return normalizeSettings({
			...settings,
			unlockedKeysByHost: {
				...settings.unlockedKeysByHost,
				[host]: Array.from(current)
			}
		});
	}
	//#endregion
	//#region src/core/engine.ts
	var MENU_ACTION_LABEL = "Pin title unmasked";
	var MENU_UNPIN_LABEL = "Unpin title mask";
	var MENU_CONTEXT_TTL_MS = 3e4;
	var MENU_ITEM_TEXT_PATTERN = /^(select|share|rename|edit name|edit chat name|move to project|add to project|move|pin chat|pin to top|pin|unpin|star|unstar|favorite|archive|delete|remove|remove from recents|duplicate|copy|copy link|download|export|选择|分享|重命名|编辑名称|编辑对话名|移动|移至项目|置顶|固定|取消置顶|归档|存档|删除|移除|复制|复制链接|下载)$/i;
	var MENU_CONTAINER_TEXT_PATTERN = /(select|share|rename|edit name|edit chat name|pin chat|pin to top|star|favorite|archive|delete|remove|move to project|add to project|copy|download|选择|分享|重命名|编辑名称|编辑对话名|置顶|固定|归档|删除|移除|移动|移至项目|复制|下载)/i;
	var MENU_ITEM_SELECTOR = "[role=\"menuitem\"], [role=\"option\"], button, [role=\"button\"], li, div";
	var MENU_CONTROL_SELECTOR = "button, [role=\"button\"], [aria-haspopup=\"menu\"], [data-testid*=\"menu\" i], [data-testid*=\"more\" i], [data-testid*=\"option\" i], [data-testid*=\"action\" i], [aria-label*=\"menu\" i], [aria-label*=\"more\" i], [aria-label*=\"option\" i], [aria-label*=\"action\" i], [aria-label*=\"overflow\" i], [aria-label*=\"conversation\" i], [aria-label*=\"chat\" i], [aria-label*=\"更多\" i]";
	function normalizeMenuText(text) {
		return text.replace(/[\uE000-\uF8FF]/g, "").replace(/\s+/g, " ").trim();
	}
	function createVisibilityIcon(document, visible) {
		const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
		svg.setAttribute("aria-hidden", "true");
		svg.setAttribute("viewBox", "0 0 24 24");
		svg.setAttribute("fill", "none");
		svg.setAttribute("stroke", "currentColor");
		svg.setAttribute("stroke-width", "2");
		svg.setAttribute("stroke-linecap", "round");
		svg.setAttribute("stroke-linejoin", "round");
		const eye = document.createElementNS("http://www.w3.org/2000/svg", "path");
		eye.setAttribute("d", "M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z");
		const pupil = document.createElementNS("http://www.w3.org/2000/svg", "circle");
		pupil.setAttribute("cx", "12");
		pupil.setAttribute("cy", "12");
		pupil.setAttribute("r", "2.5");
		svg.append(eye, pupil);
		if (!visible) {
			const slash = document.createElementNS("http://www.w3.org/2000/svg", "path");
			slash.setAttribute("d", "M4 4l16 16");
			svg.append(slash);
		}
		return svg;
	}
	function isIconOnlyMenuControl(control) {
		const text = (control.textContent ?? "").replace(/\s+/g, "").trim();
		const rect = control.getBoundingClientRect();
		const compact = rect.width === 0 || rect.height === 0 || rect.width <= 48 && rect.height <= 48;
		const className = typeof control.className === "string" ? control.className : "";
		return text.length <= 2 && compact && (Boolean(control.querySelector("svg")) || /inline-flex|items-center|justify-center|icon/i.test(className));
	}
	function isLikelyMenuControl(control) {
		const marker = [
			control.textContent,
			control.getAttribute("aria-label"),
			control.getAttribute("title"),
			control.getAttribute("data-testid"),
			control.getAttribute("aria-haspopup")
		].join(" ").replace(/\s+/g, " ").trim().toLowerCase();
		return /(\.\.\.|…|⋯|⋮|︙|···|more|menu|option|action|overflow|conversation|chat|更多|操作|菜单)/i.test(marker);
	}
	function isJsdomDocument(document) {
		return /jsdom/i.test(document.defaultView?.navigator.userAgent ?? "");
	}
	function ownerDocument(root) {
		return root.nodeType === Node.DOCUMENT_NODE ? root : root.ownerDocument;
	}
	function documentRoot(root) {
		return root.nodeType === Node.DOCUMENT_NODE ? root : root;
	}
	function setDataset(element, name, value) {
		element.dataset[`${DATA_PREFIX}${name}`] = value;
	}
	function deleteDataset(element, name) {
		delete element.dataset[`${DATA_PREFIX}${name}`];
	}
	function maskTextFor(text, pattern) {
		const patternCharacters = Array.from(pattern.trim()).slice(0, 120);
		const fallbackPattern = patternCharacters.length > 0 ? patternCharacters : ["•"];
		const targetCount = Math.min(120, Math.max(4, Array.from(text.trim()).length));
		const output = [];
		while (output.length < targetCount) output.push(...fallbackPattern);
		return output.slice(0, targetCount).join("");
	}
	function buildStyle() {
		return `
[data-${DATA_PREFIX}-title="true"] {
  position: relative !important;
  isolation: isolate !important;
}
[data-${DATA_PREFIX}-title="true"][data-${DATA_PREFIX}-state="masked"] {
  color: transparent !important;
  text-shadow: none !important;
  -webkit-text-fill-color: transparent !important;
  caret-color: transparent !important;
  text-overflow: clip !important;
}
[data-${DATA_PREFIX}-title="true"][data-${DATA_PREFIX}-state="masked"]::after {
  content: "";
  position: absolute !important;
  left: 0 !important;
  right: 0 !important;
  top: 50% !important;
  height: 0.8em !important;
  min-width: 3.5em !important;
  transform: translateY(-50%) !important;
  border-radius: 999px !important;
  background: linear-gradient(90deg, rgba(148, 163, 184, 0.38), rgba(203, 213, 225, 0.56), rgba(148, 163, 184, 0.38)) !important;
  box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.16) !important;
  pointer-events: none !important;
  z-index: 1 !important;
}
[data-${DATA_PREFIX}-title="true"][data-${DATA_PREFIX}-state="masked"][data-${DATA_PREFIX}-style="characters"]::after {
  content: attr(data-${DATA_PREFIX}-mask-text) !important;
  top: 0 !important;
  height: auto !important;
  min-width: 0 !important;
  transform: none !important;
  border-radius: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
  color: rgba(71, 85, 105, 0.78) !important;
  -webkit-text-fill-color: rgba(71, 85, 105, 0.78) !important;
  opacity: 0.72 !important;
  letter-spacing: 0 !important;
}
[data-${DATA_PREFIX}-title="true"][data-${DATA_PREFIX}-state="masked"][data-${DATA_PREFIX}-style="blur"] {
  color: inherit !important;
  -webkit-text-fill-color: currentColor !important;
  filter: blur(5px) !important;
}
[data-${DATA_PREFIX}-title="true"][data-${DATA_PREFIX}-state="masked"][data-${DATA_PREFIX}-style="blur"]::after {
  display: none !important;
}
[data-${DATA_PREFIX}-title="true"][data-${DATA_PREFIX}-state="visible"] {
  filter: none !important;
}
[data-${DATA_PREFIX}-title-clip="true"][data-${DATA_PREFIX}-clip-state="masked"] {
  color: transparent !important;
  -webkit-text-fill-color: transparent !important;
  text-overflow: clip !important;
}
[data-${DATA_PREFIX}-menu-action="true"] {
  display: flex !important;
  align-items: center !important;
  gap: 12px !important;
  width: 100% !important;
  min-height: 40px !important;
  border: 0 !important;
  border-radius: 6px !important;
  padding: 8px 12px !important;
  color: inherit !important;
  background: transparent !important;
  font: inherit !important;
  text-align: left !important;
  cursor: pointer !important;
}
[data-${DATA_PREFIX}-menu-action="true"]:hover,
[data-${DATA_PREFIX}-menu-action="true"]:focus-visible {
  background: rgba(148, 163, 184, 0.14) !important;
  outline: none !important;
}
[data-${DATA_PREFIX}-menu-action-icon="true"] {
  display: inline-flex !important;
  width: 20px !important;
  flex: 0 0 20px !important;
  align-items: center !important;
  justify-content: center !important;
  color: inherit !important;
}
[data-${DATA_PREFIX}-menu-action-icon="true"] svg {
  width: 18px !important;
  height: 18px !important;
  display: block !important;
}
`;
	}
	var MaskEngine = class {
		root;
		document;
		location;
		storage;
		adapters;
		clock;
		onError;
		rowBindings = /* @__PURE__ */ new WeakMap();
		boundRows = /* @__PURE__ */ new WeakSet();
		activeRevealKeys = /* @__PURE__ */ new Set();
		documentMenuListener = (event) => {
			this.rememberMenuRowFromDocument(event);
		};
		lastMenuRow;
		lastMenuRowAt = 0;
		observer;
		settings;
		refreshQueued = false;
		stopped = false;
		constructor(options) {
			this.root = options.root;
			this.document = ownerDocument(options.root);
			this.location = options.location;
			this.storage = options.storage;
			this.adapters = options.adapters ?? defaultAdapters;
			this.clock = options.clock ?? (() => Date.now());
			this.onError = options.onError;
		}
		async start() {
			try {
				this.stopped = false;
				this.injectStyle();
				this.settings = await this.storage.getSettings();
				await this.rescan();
				if (this.stopped) return;
				this.observer = new MutationObserver((records) => {
					if (this.shouldRefreshForMutations(records)) this.queueRefresh();
				});
				this.observer.observe(documentRoot(this.root), {
					childList: true,
					subtree: true,
					attributes: true,
					attributeFilter: [
						"href",
						"aria-current",
						"data-active",
						"data-current",
						"data-selected",
						"class"
					],
					attributeOldValue: true
				});
				this.document.addEventListener("pointerdown", this.documentMenuListener, true);
				this.document.addEventListener("click", this.documentMenuListener, true);
				this.document.addEventListener("contextmenu", this.documentMenuListener, true);
			} catch (error) {
				this.handleError(error);
			}
		}
		stop() {
			this.stopped = true;
			this.observer?.disconnect();
			this.observer = void 0;
			this.document.removeEventListener("pointerdown", this.documentMenuListener, true);
			this.document.removeEventListener("click", this.documentMenuListener, true);
			this.document.removeEventListener("contextmenu", this.documentMenuListener, true);
			this.clearMarkedTitles();
		}
		async refresh() {
			if (this.stopped) return;
			try {
				this.settings = await this.storage.getSettings();
				await this.rescan();
			} catch (error) {
				this.handleError(error);
			}
		}
		async setSettings(settings) {
			if (this.stopped) return;
			try {
				this.settings = settings;
				await this.storage.setSettings(settings);
				await this.rescan();
			} catch (error) {
				this.handleError(error);
			}
		}
		queueRefresh() {
			if (this.stopped || this.refreshQueued) return;
			this.refreshQueued = true;
			this.document.defaultView?.setTimeout(() => {
				if (this.stopped) return;
				this.refreshQueued = false;
				this.refresh();
			}, 60);
		}
		shouldRefreshForMutations(records) {
			const stateClassPattern = /\b(active|current|selected|open|expanded)\b/i;
			const elementCtor = this.document.defaultView?.HTMLElement;
			for (const record of records) {
				if (record.type === "childList") {
					if ([...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some((node) => !elementCtor || !(node instanceof elementCtor) || !node.matches(`[data-chatveil-menu-action="true"]`))) return true;
					continue;
				}
				if (record.type !== "attributes") continue;
				if (record.attributeName !== "class") return true;
				const target = record.target;
				if (!elementCtor || !(target instanceof elementCtor)) return true;
				const before = record.oldValue ?? "";
				const after = typeof target.className === "string" ? target.className : "";
				if (stateClassPattern.test(before) || stateClassPattern.test(after)) return true;
				if (target.closest(`[data-chatveil-row="true"], [data-chatveil-title="true"]`)) continue;
				return true;
			}
			return false;
		}
		injectStyle() {
			if (this.document.getElementById("chatveil-mask-style")) return;
			const style = this.document.createElement("style");
			style.id = STYLE_ID;
			style.textContent = buildStyle();
			this.document.documentElement.append(style);
		}
		clearMarkedTitles() {
			for (const element of Array.from(this.document.querySelectorAll(`[data-${DATA_PREFIX}-title="true"]`))) {
				deleteDataset(element, "Title");
				deleteDataset(element, "State");
				deleteDataset(element, "Style");
				deleteDataset(element, "MaskText");
				deleteDataset(element, "Current");
				this.clearTitleClip(element);
			}
			for (const element of Array.from(this.document.querySelectorAll(`[data-${DATA_PREFIX}-row="true"]`))) {
				deleteDataset(element, "Row");
				deleteDataset(element, "Key");
			}
		}
		async rescan() {
			if (this.stopped) return;
			const settings = this.settings ?? await this.storage.getSettings();
			this.settings = settings;
			const status = getSiteStatus(settings, this.location.hostname);
			if (!status.enabled || !canRunOnHost(settings, this.location.hostname)) {
				this.clearMarkedTitles();
				return;
			}
			const adapter = selectAdapter(this.adapters, this.location);
			if (!adapter) {
				this.clearMarkedTitles();
				return;
			}
			const now = this.clock();
			const temporaryShowAll = (settings.showAllUntilByHost[status.host] ?? 0) > now;
			const unlocked = new Set(settings.unlockedKeysByHost[status.host] ?? []);
			const seenTitles = /* @__PURE__ */ new Set();
			const items = adapter.findHistoryItems(documentRoot(this.root));
			await Promise.all(items.map(async (item, index) => {
				const rawKey = adapter.getStableKey(item, this.location, index);
				const hashedKey = await hashStableKey(`${status.host}:${adapter.id}:${rawKey}`);
				const isCurrent = adapter.isCurrent(item, this.location);
				const isVisible = isCurrent || temporaryShowAll || unlocked.has(hashedKey) || this.activeRevealKeys.has(hashedKey);
				this.applyMaskState(item, settings, hashedKey, isCurrent, isVisible);
				seenTitles.add(item.title);
			}));
			for (const element of Array.from(this.document.querySelectorAll(`[data-${DATA_PREFIX}-title="true"]`))) if (!seenTitles.has(element)) {
				deleteDataset(element, "Title");
				deleteDataset(element, "State");
				deleteDataset(element, "Style");
				deleteDataset(element, "MaskText");
				deleteDataset(element, "Current");
				this.clearTitleClip(element);
			}
			this.injectMenuAction(status.host);
		}
		applyMaskState(item, settings, hashedKey, isCurrent, isVisible) {
			setDataset(item.row, "Row", "true");
			setDataset(item.row, "Key", hashedKey);
			setDataset(item.title, "Title", "true");
			setDataset(item.title, "Current", String(isCurrent));
			setDataset(item.title, "State", isVisible ? "visible" : "masked");
			setDataset(item.title, "Style", settings.maskStyle);
			setDataset(item.title, "MaskText", maskTextFor(item.title.textContent ?? "", settings.customMaskChar));
			this.applyTitleClipState(item.title, isVisible ? "visible" : "masked");
			this.rowBindings.set(item.row, {
				host: this.location.hostname.toLowerCase(),
				hashedKey,
				title: item.title
			});
			this.bindRow(item.row);
		}
		bindRow(row) {
			if (this.boundRows.has(row)) return;
			this.boundRows.add(row);
			row.addEventListener("pointerenter", () => {
				this.revealRow(row);
			});
			row.addEventListener("pointerleave", () => {
				this.maskRow(row);
			});
			row.addEventListener("focusin", () => {
				this.revealRow(row);
			});
			row.addEventListener("focusout", () => {
				this.maskRow(row);
			});
			row.addEventListener("pointerdown", (event) => {
				this.rememberMenuRow(row, event);
			}, true);
			row.addEventListener("click", (event) => {
				this.rememberMenuRow(row, event);
			}, true);
			row.addEventListener("contextmenu", () => {
				this.rememberMenuRowFor(row);
			});
			row.addEventListener("click", (event) => {
				if (event.detail !== 3) return;
				event.preventDefault();
				event.stopPropagation();
				this.toggleUnlock(row);
			});
		}
		revealRow(row) {
			if (this.stopped) return;
			const binding = this.rowBindings.get(row);
			if (!binding) return;
			this.activeRevealKeys.add(binding.hashedKey);
			this.setBoundTitleState(binding, "visible");
		}
		maskRow(row) {
			if (this.stopped) return;
			const binding = this.rowBindings.get(row);
			if (!binding) return;
			this.activeRevealKeys.delete(binding.hashedKey);
			this.setBoundTitleState(binding, this.shouldKeepBoundTitleVisible(binding) ? "visible" : "masked");
		}
		setBoundTitleState(binding, state) {
			if (!this.document.documentElement.contains(binding.title)) return;
			setDataset(binding.title, "State", state);
			this.applyTitleClipState(binding.title, state);
		}
		shouldKeepBoundTitleVisible(binding) {
			if (binding.title.dataset[`chatveilCurrent`] === "true") return true;
			if (this.activeRevealKeys.has(binding.hashedKey)) return true;
			const settings = this.settings;
			if (!settings) return false;
			if ((settings.showAllUntilByHost[binding.host] ?? 0) > this.clock()) return true;
			return new Set(settings.unlockedKeysByHost[binding.host] ?? []).has(binding.hashedKey);
		}
		applyTitleClipState(title, state) {
			const parent = title.parentElement;
			if (!parent || !this.isTitleClipParent(title, parent)) {
				this.clearTitleClip(title);
				return;
			}
			setDataset(parent, "TitleClip", "true");
			setDataset(parent, "ClipState", state);
		}
		clearTitleClip(title) {
			const parent = title.parentElement;
			if (!parent || parent.dataset[`chatveilTitleClip`] !== "true") return;
			deleteDataset(parent, "TitleClip");
			deleteDataset(parent, "ClipState");
		}
		isTitleClipParent(title, parent) {
			if (title.dataset.chatveilTextWrapper !== "true") return false;
			if (parent.querySelector("button, [role=\"button\"], img, svg, canvas, picture, video, [role=\"img\"]")) return false;
			const className = typeof parent.className === "string" ? parent.className : "";
			return /\b(flex-?1|min-w-0|w-full|truncate|text-ellipsis|line-clamp|overflow-hidden)\b/i.test(className);
		}
		async toggleUnlock(row) {
			if (this.stopped) return;
			const binding = this.rowBindings.get(row);
			const settings = this.settings;
			if (!binding || !settings) return;
			const host = binding.host;
			const shouldUnlock = !new Set(settings.unlockedKeysByHost[host] ?? []).has(binding.hashedKey);
			const next = setUnlockedKey(settings, host, binding.hashedKey, shouldUnlock);
			await this.setSettings(next);
		}
		rememberMenuRow(row, event) {
			const target = event.target;
			if (!(target instanceof Element)) return;
			const control = target.closest(MENU_CONTROL_SELECTOR);
			if (!control || !row.contains(control) || !isLikelyMenuControl(control) && !isIconOnlyMenuControl(control)) return;
			this.rememberMenuRowFor(row);
		}
		rememberMenuRowFromDocument(event) {
			if (this.stopped) return;
			const target = event.target;
			if (!(target instanceof Element)) return;
			const control = target.closest(MENU_CONTROL_SELECTOR);
			if (!control || !isLikelyMenuControl(control) && !isIconOnlyMenuControl(control)) return;
			const row = this.findRowForMenuControl(control);
			if (row) this.rememberMenuRowFor(row);
		}
		rememberMenuRowFor(row) {
			this.lastMenuRow = row;
			this.lastMenuRowAt = this.clock();
			this.queueMenuRefresh();
		}
		queueMenuRefresh() {
			this.queueRefresh();
			for (const delay of [90, 240]) this.document.defaultView?.setTimeout(() => {
				if (!this.stopped) this.queueRefresh();
			}, delay);
		}
		findRowForMenuControl(control) {
			const ancestor = control.closest(`[data-${DATA_PREFIX}-row="true"]`);
			if (ancestor && this.rowBindings.has(ancestor)) return ancestor;
			const rows = Array.from(this.document.querySelectorAll(`[data-${DATA_PREFIX}-row="true"]`)).filter((row) => this.rowBindings.has(row));
			if (rows.length === 0) return void 0;
			const controlRect = control.getBoundingClientRect();
			const controlCenterY = controlRect.top + controlRect.height / 2;
			const controlCenterX = controlRect.left + controlRect.width / 2;
			let best;
			for (const row of rows) {
				const rowRect = row.getBoundingClientRect();
				const rowCenterY = rowRect.top + rowRect.height / 2;
				const rowCenterX = rowRect.left + rowRect.width / 2;
				const verticalDistance = controlCenterY >= rowRect.top - 12 && controlCenterY <= rowRect.bottom + 12 ? 0 : Math.abs(controlCenterY - rowCenterY);
				if (verticalDistance > 64 && rowRect.height > 0) continue;
				const horizontalDistance = Math.min(Math.abs(controlRect.left - rowRect.left), Math.abs(controlRect.left - rowRect.right), Math.abs(controlRect.right - rowRect.right), Math.abs(controlCenterX - rowCenterX));
				const score = verticalDistance * 10 + horizontalDistance;
				if (!best || score < best.score) best = {
					row,
					score
				};
			}
			return best?.row;
		}
		injectMenuAction(host) {
			const menus = this.findMenus();
			if (host === "z.ai" || host.endsWith(".z.ai")) {
				this.cleanupMenuActions([]);
				return;
			}
			const menu = this.chooseActionMenu(menus);
			if (!menu) {
				this.cleanupMenuActions([]);
				return;
			}
			const row = this.resolveMenuRow(menu);
			if (!row) {
				this.cleanupMenuActions(menus);
				return;
			}
			const binding = this.rowBindings.get(row);
			const settings = this.settings;
			if (!binding || !settings) {
				this.cleanupMenuActions(menus);
				return;
			}
			this.cleanupMenuActions([menu]);
			const unlocked = new Set(settings.unlockedKeysByHost[host] ?? []).has(binding.hashedKey);
			const actions = this.directMenuActions(menu);
			const existing = actions.find((action) => action instanceof HTMLButtonElement);
			for (const action of actions) if (action !== existing) action.remove();
			if (existing) {
				this.updateMenuAction(existing, row, unlocked);
				this.positionMenuAction(menu, existing);
				return;
			}
			const action = this.createMenuAction(row, unlocked);
			this.positionMenuAction(menu, action);
		}
		resolveMenuRow(menu) {
			const row = this.lastMenuRow;
			if (row && this.document.documentElement.contains(row) && this.rowBindings.has(row) && this.clock() - this.lastMenuRowAt <= MENU_CONTEXT_TTL_MS) return row;
			return this.findNearestRowForMenu(menu);
		}
		findNearestRowForMenu(menu) {
			const rows = Array.from(this.document.querySelectorAll(`[data-${DATA_PREFIX}-row="true"]`)).filter((row) => this.rowBindings.has(row));
			if (rows.length === 0) return void 0;
			const menuRect = menu.getBoundingClientRect();
			if (menuRect.width <= 0 && menuRect.height <= 0) return void 0;
			const menuAnchorY = menuRect.top;
			const menuAnchorX = menuRect.left;
			let best;
			for (const row of rows) {
				const rowRect = row.getBoundingClientRect();
				if (rowRect.width <= 0 && rowRect.height <= 0) continue;
				const rowCenterY = rowRect.top + rowRect.height / 2;
				const verticalDistance = Math.abs(menuAnchorY - rowCenterY);
				if (verticalDistance > 140) continue;
				const horizontalDistance = Math.min(Math.abs(menuAnchorX - rowRect.right), Math.abs(menuAnchorX - rowRect.left), Math.abs(menuAnchorX - (rowRect.left + rowRect.width / 2)));
				const score = verticalDistance * 10 + horizontalDistance;
				if (!best || score < best.score) best = {
					row,
					score
				};
			}
			return best?.row;
		}
		createMenuAction(row, unlocked) {
			const action = this.document.createElement("button");
			action.type = "button";
			action.setAttribute("role", "menuitem");
			setDataset(action, "MenuAction", "true");
			const icon = this.document.createElement("span");
			setDataset(icon, "MenuActionIcon", "true");
			const label = this.document.createElement("span");
			action.append(icon, label);
			this.updateMenuAction(action, row, unlocked);
			return action;
		}
		updateMenuAction(action, row, unlocked) {
			action.querySelector(`[data-${DATA_PREFIX}-menu-action-icon="true"]`)?.replaceChildren(createVisibilityIcon(this.document, !unlocked));
			const label = action.querySelector("span:last-child");
			if (label) label.textContent = unlocked ? MENU_UNPIN_LABEL : MENU_ACTION_LABEL;
			action.onclick = (event) => {
				event.preventDefault();
				event.stopPropagation();
				const binding = this.rowBindings.get(row);
				const settings = this.settings;
				if (!binding || !settings) return;
				const isUnlocked = new Set(settings.unlockedKeysByHost[binding.host] ?? []).has(binding.hashedKey);
				this.updateMenuAction(action, row, !isUnlocked);
				this.toggleUnlock(row).then(() => {
					if (!this.stopped) this.injectMenuAction(binding.host);
				}).catch((error) => {
					this.handleError(error);
				});
			};
		}
		findMenus() {
			const menus = new Set(this.findPopoverMenuBodies());
			const items = Array.from(this.document.querySelectorAll(MENU_ITEM_SELECTOR)).filter((element) => {
				if (element.closest(`[data-chatveil-menu-action="true"]`)) return false;
				return MENU_ITEM_TEXT_PATTERN.test(normalizeMenuText(element.textContent ?? ""));
			});
			for (const item of items) {
				const menu = this.findMenuContainer(item);
				if (menu) menus.add(menu);
			}
			return Array.from(menus);
		}
		findPopoverMenuBodies() {
			const popovers = Array.from(this.document.querySelectorAll("[class*=\"z-popover\"], [class*=\"popover\" i], [data-radix-popper-content-wrapper], [data-floating-ui-portal], [data-headlessui-portal]"));
			const menus = [];
			for (const popover of popovers) {
				const candidates = [popover, ...Array.from(popover.querySelectorAll("[role=\"menu\"], [class*=\"overflow-y-auto\"], [class*=\"min-h-0\"], [class*=\"rounded\"], [class*=\"p-1\"]"))];
				for (const candidate of candidates) {
					if (menus.includes(candidate)) continue;
					if (this.hasDirectMenuShape(candidate) && this.isEligibleMenuContainer(candidate)) menus.push(candidate);
				}
			}
			return menus;
		}
		hasDirectMenuShape(menu) {
			const directItems = Array.from(menu.children).filter((child) => {
				if (!(child instanceof HTMLElement)) return false;
				if (child.matches(`[data-chatveil-menu-action="true"]`)) return false;
				return child.matches("[role=\"menuitem\"], [role=\"option\"], button, [role=\"button\"]");
			});
			if (directItems.length < 2) return false;
			return directItems.some((item) => MENU_ITEM_TEXT_PATTERN.test(normalizeMenuText(item.textContent ?? ""))) && MENU_CONTAINER_TEXT_PATTERN.test(menu.textContent ?? "");
		}
		chooseActionMenu(menus) {
			return menus.find((menu) => Boolean(this.findMenuItemByText(menu, /^(pin chat|pin to top|pin|置顶|固定)$/i))) ?? menus.find((menu) => Boolean(this.findMenuItemByText(menu, /^(delete|remove|删除|移除)$/i))) ?? menus[0];
		}
		directMenuActions(menu) {
			return Array.from(menu.children).filter((child) => child instanceof HTMLElement && child.matches(`[data-chatveil-menu-action="true"]`));
		}
		positionMenuAction(menu, action) {
			const pinItem = this.findMenuItemByText(menu, /^(pin chat|pin to top|pin|置顶|固定)$/i);
			const destructiveItem = this.findMenuItemByText(menu, /^(delete|remove|删除|移除)$/i);
			const insertBefore = pinItem ?? destructiveItem;
			if (insertBefore?.parentElement === menu) {
				if (action.nextElementSibling !== insertBefore) menu.insertBefore(action, insertBefore);
				return;
			}
			if (action.parentElement !== menu || action.nextElementSibling) menu.append(action);
		}
		findMenuContainer(item) {
			let current = item.parentElement;
			for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
				if (current === this.document.body || current === this.document.documentElement) break;
				if (current.matches("[role=\"menuitem\"], [role=\"option\"], button, [role=\"button\"], li")) continue;
				if (current.matches(`[data-chatveil-menu-action="true"]`) || current.closest(`[data-chatveil-menu-action="true"]`)) continue;
				const text = current.textContent ?? "";
				if ((Array.from(current.querySelectorAll(MENU_ITEM_SELECTOR)).filter((element) => !element.closest(`[data-chatveil-menu-action="true"]`) && MENU_ITEM_TEXT_PATTERN.test(normalizeMenuText(element.textContent ?? ""))).length >= 2 && MENU_CONTAINER_TEXT_PATTERN.test(text) || current.getAttribute("role") === "menu") && this.isEligibleMenuContainer(current)) return current;
			}
		}
		isEligibleMenuContainer(menu) {
			if (!this.document.documentElement.contains(menu)) return false;
			if (menu.matches("[role=\"menuitem\"], [role=\"option\"], button, [role=\"button\"], li")) return false;
			if (menu.closest(`[data-chatveil-title="true"], [data-chatveil-row="true"]`)) return false;
			const style = this.document.defaultView?.getComputedStyle(menu);
			if (style && (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")) return false;
			if (isJsdomDocument(this.document)) return true;
			if (!this.hasPopupMenuSignal(menu)) return false;
			const rect = menu.getBoundingClientRect();
			if (rect.width <= 0 || rect.height <= 0) return false;
			if (rect.width > 720 || rect.height > 900) return false;
			const viewportWidth = this.document.defaultView?.innerWidth ?? 0;
			const viewportHeight = this.document.defaultView?.innerHeight ?? 0;
			if (viewportWidth > 0 && rect.width > viewportWidth * .8) return false;
			if (viewportHeight > 0 && rect.height > viewportHeight * .8) return false;
			return true;
		}
		hasPopupMenuSignal(menu) {
			const popupSelector = [
				"[role=\"menu\"]",
				"[role=\"listbox\"]",
				"[data-radix-menu-content]",
				"[data-radix-popper-content-wrapper]",
				"[data-headlessui-portal]",
				"[data-floating-ui-portal]",
				"[data-popper-placement]",
				"[data-side][data-align]",
				"[class*=\"popover\" i]",
				"[class*=\"dropdown\" i]",
				"[class*=\"floating\" i]",
				"[class*=\"portal\" i]"
			].join(",");
			if (menu.matches(popupSelector) || menu.closest(popupSelector)) return true;
			const marker = [
				menu.id,
				menu.className,
				menu.getAttribute("data-testid"),
				menu.getAttribute("data-radix-menu-content"),
				menu.getAttribute("data-popper-placement")
			].join(" ").toLowerCase();
			if (/\b(popover|dropdown|menu|popup|portal|floating|radix|headlessui|tooltip|tippy)\b/.test(marker)) return true;
			let current = menu;
			for (let depth = 0; current && depth < 4; depth += 1, current = current.parentElement) {
				if (current === this.document.body || current === this.document.documentElement) break;
				const style = this.document.defaultView?.getComputedStyle(current);
				if (!style) continue;
				const zIndex = Number.parseInt(style.zIndex, 10);
				const hasElevatedLayer = Number.isFinite(zIndex) && zIndex >= 10;
				const hasPopupPosition = style.position === "fixed" || style.position === "absolute";
				const hasPopupPaint = style.boxShadow !== "none" || style.transform !== "none";
				if (hasPopupPosition && (hasElevatedLayer || hasPopupPaint)) return true;
			}
			return false;
		}
		cleanupMenuActions(validMenus) {
			const menus = new Set(validMenus);
			for (const action of Array.from(this.document.querySelectorAll(`[data-${DATA_PREFIX}-menu-action="true"]`))) {
				const menu = validMenus.find((candidate) => action.parentElement === candidate);
				if (!menu || !menus.has(menu)) action.remove();
			}
			for (const menu of validMenus) {
				const actions = this.directMenuActions(menu);
				const keep = actions[0];
				for (const action of actions.slice(1)) action.remove();
				if (keep) this.positionMenuAction(menu, keep);
			}
		}
		findMenuItemByText(menu, pattern) {
			return Array.from(menu.children).find((child) => {
				return child instanceof HTMLElement && pattern.test(normalizeMenuText(child.textContent ?? ""));
			});
		}
		handleError(error) {
			this.onError?.(error);
		}
	};
	//#endregion
	//#region src/userscript/main.ts
	var PANEL_ID = "chatveil-userscript-panel";
	function escapeAttribute(value) {
		return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
	}
	var userscriptStorage = {
		async getSettings() {
			return normalizeSettings(GM_getValue(SETTINGS_KEY, void 0));
		},
		async setSettings(settings) {
			GM_setValue(SETTINGS_KEY, normalizeSettings(settings));
		}
	};
	var engine = new MaskEngine({
		root: document,
		location: window.location,
		storage: userscriptStorage
	});
	function renderPanel(settings) {
		const site = getSiteStatus(settings, window.location.hostname);
		const panel = document.createElement("form");
		panel.id = PANEL_ID;
		panel.innerHTML = `
    <style>
      #${PANEL_ID} {
        position: fixed;
        right: 16px;
        bottom: 16px;
        z-index: 2147483647;
        display: grid;
        gap: 8px;
        width: 236px;
        padding: 12px;
        border: 1px solid rgba(100, 116, 139, 0.32);
        border-radius: 8px;
        color: #172033;
        background: rgba(255, 255, 255, 0.96);
        box-shadow: 0 16px 38px rgba(15, 23, 42, 0.22);
        font: 13px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      }
      #${PANEL_ID} label {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
      }
      #${PANEL_ID} select,
      #${PANEL_ID} input[type="number"],
      #${PANEL_ID} input[type="text"] {
        width: 112px;
      }
      #${PANEL_ID} button {
        min-height: 30px;
      }
      #${PANEL_ID} .row {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 6px;
      }
      @media (prefers-color-scheme: dark) {
        #${PANEL_ID} {
          color: #e6eef7;
          background: rgba(17, 24, 39, 0.96);
        }
      }
    </style>
    <strong>ChatVeil</strong>
    <label>Enabled <input name="enabled" type="checkbox" ${site.enabled ? "checked" : ""}></label>
    <label>
      Style
      <select name="style">
        <option value="skeleton" ${settings.maskStyle === "skeleton" ? "selected" : ""}>Skeleton</option>
        <option value="characters" ${settings.maskStyle === "characters" ? "selected" : ""}>Characters</option>
        <option value="blur" ${settings.maskStyle === "blur" ? "selected" : ""}>Blur</option>
      </select>
    </label>
    <label>Character <input name="char" type="text" maxlength="120" value="${escapeAttribute(settings.customMaskChar)}"></label>
    <div class="row">
      <button name="show" type="button">Show 10m</button>
      <button name="reset" type="button">Reset</button>
    </div>
    <button name="close" type="button">Close</button>
  `;
		return panel;
	}
	async function showPanel() {
		document.getElementById(PANEL_ID)?.remove();
		const settings = await userscriptStorage.getSettings();
		const panel = renderPanel(settings);
		document.body.append(panel);
		panel.addEventListener("change", async (event) => {
			const target = event.target;
			if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) return;
			const form = new FormData(panel);
			const enabled = form.get("enabled") === "on";
			const next = normalizeSettings({
				...settings,
				maskStyle: form.get("style"),
				customMaskChar: form.get("char")
			});
			const withHost = enabled ? addCustomHost(next, window.location.hostname) : updateHostEnabled(removeCustomHost(next, window.location.hostname), window.location.hostname, false);
			await userscriptStorage.setSettings(withHost);
			await engine.refresh();
		});
		panel.addEventListener("click", async (event) => {
			const target = event.target;
			if (!(target instanceof HTMLButtonElement)) return;
			const current = await userscriptStorage.getSettings();
			if (target.name === "close") {
				panel.remove();
				return;
			}
			if (target.name === "show") await userscriptStorage.setSettings(normalizeSettings({
				...current,
				showAllUntilByHost: {
					...current.showAllUntilByHost,
					[window.location.hostname]: Date.now() + 600 * 1e3
				}
			}));
			if (target.name === "reset") await userscriptStorage.setSettings(normalizeSettings({
				...current,
				unlockedKeysByHost: {
					...current.unlockedKeysByHost,
					[window.location.hostname]: []
				}
			}));
			await engine.refresh();
		});
	}
	GM_registerMenuCommand("Open ChatVeil controls", () => {
		showPanel();
	});
	engine.start();
	//#endregion
})();

//# sourceMappingURL=chatveil.user.js.map