feat: aggressive mode — service-agnostic interception
Replace URL-pattern and body-shape matching with universal approach: Outbound (substitution): - Hooks ALL POST/PUT/PATCH fetch and XHR requests, not just known API endpoints. Skips static assets and analytics. - Deep-walks any JSON structure recursively to find and substitute all string values. Skips metadata keys (model, id, token, etc.). - Falls back to raw string substitution for non-JSON bodies. Inbound (reveal): - Walks ALL text nodes in document.body, not just specific CSS selectors. Skips SCRIPT, STYLE, INPUT, TEXTAREA tags. - Handles streaming by observing characterData mutations globally. This makes Silent Send survive any API restructuring — the only thing that could break it is a site encrypting request bodies in JS before fetch, which would also break their own dev tools. Performance: reveal pairs are cached and only rebuilt on config change. Deep walk skips known non-content keys to avoid touching auth tokens or request metadata. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
+153
-181
@@ -285,8 +285,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Process a JSON body — handles all known AI service API shapes
|
// Deep JSON scanner — finds and substitutes ALL strings in
|
||||||
|
// any JSON structure. Service-agnostic. Survives API changes.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
const SKIP_KEYS = new Set([
|
||||||
|
// Keys that should never be modified (auth, metadata, IDs)
|
||||||
|
'model', 'id', 'parent_message_id', 'conversation_id',
|
||||||
|
'organization_id', 'uuid', 'token', 'api_key', 'key',
|
||||||
|
'authorization', 'cookie', 'csrf', 'nonce', 'hash',
|
||||||
|
'Content-Type', 'content-type', 'Accept', 'accept',
|
||||||
|
'User-Agent', 'user-agent', 'x-request-id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Min length for a string to be worth scanning
|
||||||
|
const MIN_STRING_LENGTH = 2;
|
||||||
|
|
||||||
function processBody(body) {
|
function processBody(body) {
|
||||||
let modified = false;
|
let modified = false;
|
||||||
const allReplacements = [];
|
const allReplacements = [];
|
||||||
@@ -300,94 +313,44 @@
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk any string values that look like user content
|
// Recursively walk any JSON structure and substitute all strings
|
||||||
function walkAndSubstitute(obj, key) {
|
function deepWalk(obj, parentKey) {
|
||||||
if (typeof obj[key] === 'string' && obj[key].length > 0) {
|
if (typeof obj === 'string') {
|
||||||
const r = processText(obj[key]);
|
if (obj.length >= MIN_STRING_LENGTH) {
|
||||||
if (r.modified) obj[key] = r.text;
|
return processText(obj);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Claude: { prompt: "..." } ---
|
|
||||||
if (typeof body.prompt === 'string') {
|
|
||||||
walkAndSubstitute(body, 'prompt');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Claude / OpenAI / OpenWebUI: { messages: [{ role, content }] } ---
|
|
||||||
if (Array.isArray(body.messages)) {
|
|
||||||
for (const msg of body.messages) {
|
|
||||||
if (msg.role !== 'user' && msg.role !== 'human') continue;
|
|
||||||
|
|
||||||
if (typeof msg.content === 'string') {
|
|
||||||
walkAndSubstitute(msg, 'content');
|
|
||||||
}
|
}
|
||||||
|
return { text: obj, modified: false };
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(msg.content)) {
|
if (Array.isArray(obj)) {
|
||||||
for (let j = 0; j < msg.content.length; j++) {
|
for (let i = 0; i < obj.length; i++) {
|
||||||
const part = msg.content[j];
|
if (typeof obj[i] === 'string' && obj[i].length >= MIN_STRING_LENGTH) {
|
||||||
if (typeof part === 'string') {
|
const r = processText(obj[i]);
|
||||||
const r = processText(part);
|
if (r.modified) obj[i] = r.text;
|
||||||
if (r.modified) msg.content[j] = r.text;
|
} else if (typeof obj[i] === 'object' && obj[i] !== null) {
|
||||||
} else if (part?.type === 'text' && typeof part.text === 'string') {
|
deepWalk(obj[i], null);
|
||||||
walkAndSubstitute(part, 'text');
|
}
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object' && obj !== null) {
|
||||||
|
for (const key of Object.keys(obj)) {
|
||||||
|
// Skip metadata/auth keys
|
||||||
|
if (SKIP_KEYS.has(key)) continue;
|
||||||
|
|
||||||
|
const val = obj[key];
|
||||||
|
if (typeof val === 'string' && val.length >= MIN_STRING_LENGTH) {
|
||||||
|
const r = processText(val);
|
||||||
|
if (r.modified) obj[key] = r.text;
|
||||||
|
} else if (typeof val === 'object' && val !== null) {
|
||||||
|
deepWalk(val, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Claude: { content: [{ type: "text", text }] } ---
|
deepWalk(body, null);
|
||||||
if (Array.isArray(body.content) && !Array.isArray(body.messages)) {
|
|
||||||
for (let i = 0; i < body.content.length; i++) {
|
|
||||||
const item = body.content[i];
|
|
||||||
if (item.type === 'text' && typeof item.text === 'string') {
|
|
||||||
walkAndSubstitute(item, 'text');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ChatGPT: { action: "next", messages: [{ content: { parts: ["..."] } }] } ---
|
|
||||||
if (Array.isArray(body.messages)) {
|
|
||||||
for (const msg of body.messages) {
|
|
||||||
if (msg.content?.parts && Array.isArray(msg.content.parts)) {
|
|
||||||
for (let i = 0; i < msg.content.parts.length; i++) {
|
|
||||||
if (typeof msg.content.parts[i] === 'string') {
|
|
||||||
const r = processText(msg.content.parts[i]);
|
|
||||||
if (r.modified) msg.content.parts[i] = r.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Gemini: nested prompts in f.req batch RPC (text strings in arrays) ---
|
|
||||||
// Gemini uses a complex nested array format. We recursively find strings.
|
|
||||||
if (Array.isArray(body) || body?.fReq) {
|
|
||||||
function walkArray(arr) {
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
if (typeof arr[i] === 'string' && arr[i].length > 2) {
|
|
||||||
const r = processText(arr[i]);
|
|
||||||
if (r.modified) arr[i] = r.text;
|
|
||||||
} else if (Array.isArray(arr[i])) {
|
|
||||||
walkArray(arr[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Array.isArray(body)) walkArray(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Grok: { message: "...", messages: [...] } ---
|
|
||||||
if (typeof body.message === 'string') {
|
|
||||||
walkAndSubstitute(body, 'message');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- OpenWebUI: { prompt: "...", messages: [...], model: "..." } ---
|
|
||||||
// Already handled by 'prompt' and 'messages' above
|
|
||||||
|
|
||||||
// --- Generic: { query: "..." } or { input: "..." } ---
|
|
||||||
if (typeof body.query === 'string') walkAndSubstitute(body, 'query');
|
|
||||||
if (typeof body.input === 'string') walkAndSubstitute(body, 'input');
|
|
||||||
|
|
||||||
return { modified, replacements: allReplacements };
|
return { modified, replacements: allReplacements };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,48 +366,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// API URL Detection — matches known AI service endpoints
|
// Fetch Interception — scans ALL POST requests with a body.
|
||||||
// ============================================================
|
// Service-agnostic: doesn't depend on URL patterns.
|
||||||
const API_PATTERNS = [
|
|
||||||
// Claude
|
|
||||||
/\/api\/organizations\/.*\/(chat_conversations|completion|messages)/,
|
|
||||||
// ChatGPT / OpenAI
|
|
||||||
/\/backend-api\/conversation/,
|
|
||||||
/\/api\/conversation/,
|
|
||||||
/\/v1\/chat\/completions/,
|
|
||||||
// Grok
|
|
||||||
/\/i\/api\/graphql.*grok/i,
|
|
||||||
/grok.*\/api\//,
|
|
||||||
/\/2\/grok\/add_response/,
|
|
||||||
// Gemini
|
|
||||||
/\/_\/BardChatUi\/data\//,
|
|
||||||
/\/google\.internal\.gemini/,
|
|
||||||
/generativelanguage.*generateContent/,
|
|
||||||
// OpenWebUI (self-hosted, various paths)
|
|
||||||
/\/api\/chat\/?/,
|
|
||||||
/\/ollama\/api\/chat/,
|
|
||||||
/\/api\/v1\/chat\/completions/,
|
|
||||||
];
|
|
||||||
|
|
||||||
function isTargetApiUrl(url) {
|
|
||||||
return API_PATTERNS.some(pattern => pattern.test(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Fetch Interception
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const originalFetch = window.fetch;
|
const originalFetch = window.fetch;
|
||||||
|
|
||||||
|
// URLs to never touch (static assets, analytics, etc.)
|
||||||
|
const SKIP_URL_PATTERNS = [
|
||||||
|
/\.(js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|ico)(\?|$)/i,
|
||||||
|
/\/analytics\//i,
|
||||||
|
/\/telemetry\//i,
|
||||||
|
/\/log\//i,
|
||||||
|
/google-analytics/i,
|
||||||
|
/sentry/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
function shouldSkipUrl(url) {
|
||||||
|
return SKIP_URL_PATTERNS.some(p => p.test(url));
|
||||||
|
}
|
||||||
|
|
||||||
window.fetch = async function (url, options) {
|
window.fetch = async function (url, options) {
|
||||||
if (!settings.enabled || !hasSubstitutions()) {
|
if (!settings.enabled || !hasSubstitutions()) {
|
||||||
return originalFetch.call(this, url, options);
|
return originalFetch.call(this, url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlStr = typeof url === 'string' ? url : url?.url || '';
|
const urlStr = typeof url === 'string' ? url : url?.url || '';
|
||||||
|
const method = (options?.method || 'GET').toUpperCase();
|
||||||
|
|
||||||
if (isTargetApiUrl(urlStr) && options?.body && typeof options.body === 'string') {
|
// Only intercept POST/PUT/PATCH with a string body
|
||||||
|
if (
|
||||||
|
(method === 'POST' || method === 'PUT' || method === 'PATCH') &&
|
||||||
|
options?.body && typeof options.body === 'string' &&
|
||||||
|
!shouldSkipUrl(urlStr)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// Try JSON first (most services)
|
// Try JSON
|
||||||
const body = JSON.parse(options.body);
|
const body = JSON.parse(options.body);
|
||||||
const { modified, replacements } = processBody(body);
|
const { modified, replacements } = processBody(body);
|
||||||
|
|
||||||
@@ -452,23 +408,21 @@
|
|||||||
options = { ...options, body: JSON.stringify(body) };
|
options = { ...options, body: JSON.stringify(body) };
|
||||||
notifySubstitutions(replacements);
|
notifySubstitutions(replacements);
|
||||||
console.log(
|
console.log(
|
||||||
`[Silent Send] Substituted ${replacements.length} value(s) in fetch request`
|
`[Silent Send] Substituted ${replacements.length} value(s) in ${urlStr}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not JSON — try form-encoded (Gemini uses f.req param)
|
// Not JSON — try raw string substitution (form data, etc.)
|
||||||
try {
|
if (options.body.length > MIN_STRING_LENGTH) {
|
||||||
if (options.body.includes('f.req=') || options.body.includes('at=')) {
|
const result = substituteAll(options.body);
|
||||||
const result = substituteAll(options.body);
|
if (result.modified) {
|
||||||
if (result.modified) {
|
options = { ...options, body: result.text };
|
||||||
options = { ...options, body: result.text };
|
notifySubstitutions(result.replacements);
|
||||||
notifySubstitutions(result.replacements);
|
console.log(
|
||||||
console.log(
|
`[Silent Send] Substituted ${result.replacements.length} value(s) in form body`
|
||||||
`[Silent Send] Substituted ${result.replacements.length} value(s) in form request`
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e2) { /* pass through */ }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,21 +430,24 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// XMLHttpRequest Interception (fallback)
|
// XMLHttpRequest Interception — same aggressive approach
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const origOpen = XMLHttpRequest.prototype.open;
|
const origOpen = XMLHttpRequest.prototype.open;
|
||||||
const origSend = XMLHttpRequest.prototype.send;
|
const origSend = XMLHttpRequest.prototype.send;
|
||||||
|
|
||||||
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
||||||
this._ssUrl = url;
|
this._ssUrl = url;
|
||||||
|
this._ssMethod = method;
|
||||||
return origOpen.call(this, method, url, ...rest);
|
return origOpen.call(this, method, url, ...rest);
|
||||||
};
|
};
|
||||||
|
|
||||||
XMLHttpRequest.prototype.send = function (body) {
|
XMLHttpRequest.prototype.send = function (body) {
|
||||||
|
const method = (this._ssMethod || 'GET').toUpperCase();
|
||||||
if (
|
if (
|
||||||
settings.enabled && hasSubstitutions() &&
|
settings.enabled && hasSubstitutions() &&
|
||||||
typeof body === 'string' && this._ssUrl &&
|
(method === 'POST' || method === 'PUT' || method === 'PATCH') &&
|
||||||
isTargetApiUrl(this._ssUrl)
|
typeof body === 'string' && body.length > MIN_STRING_LENGTH &&
|
||||||
|
!shouldSkipUrl(this._ssUrl || '')
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(body);
|
const parsed = JSON.parse(body);
|
||||||
@@ -499,7 +456,14 @@
|
|||||||
body = JSON.stringify(parsed);
|
body = JSON.stringify(parsed);
|
||||||
notifySubstitutions(replacements);
|
notifySubstitutions(replacements);
|
||||||
}
|
}
|
||||||
} catch (e) { /* pass through */ }
|
} catch (e) {
|
||||||
|
// Not JSON — raw string
|
||||||
|
const result = substituteAll(body);
|
||||||
|
if (result.modified) {
|
||||||
|
body = result.text;
|
||||||
|
notifySubstitutions(result.replacements);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return origSend.call(this, body);
|
return origSend.call(this, body);
|
||||||
};
|
};
|
||||||
@@ -508,24 +472,6 @@
|
|||||||
// Response Reveal — swaps fake data back to real in the page
|
// Response Reveal — swaps fake data back to real in the page
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// All response/output selectors across supported services
|
|
||||||
const RESPONSE_SELECTORS = [
|
|
||||||
// Claude
|
|
||||||
'[data-is-streaming]', '.font-claude-message',
|
|
||||||
// Claude artifacts / code
|
|
||||||
'.code-block__code', '.artifact-content', 'pre code', 'pre',
|
|
||||||
// ChatGPT
|
|
||||||
'[data-message-author-role="assistant"]', '.markdown',
|
|
||||||
// Grok
|
|
||||||
'[class*="message-bubble"]', '[class*="response"]',
|
|
||||||
// Gemini
|
|
||||||
'.model-response-text', '.response-content', 'message-content',
|
|
||||||
// Generic / OpenWebUI
|
|
||||||
'.prose', '[class*="Message"]', '[class*="assistant"]',
|
|
||||||
// Code and artifacts everywhere
|
|
||||||
'code', '.hljs', '.highlight',
|
|
||||||
].join(', ');
|
|
||||||
|
|
||||||
// Build reverse mapping pairs from identity + explicit mappings
|
// Build reverse mapping pairs from identity + explicit mappings
|
||||||
function buildRevealPairs() {
|
function buildRevealPairs() {
|
||||||
const pairs = [];
|
const pairs = [];
|
||||||
@@ -564,8 +510,15 @@
|
|||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache reveal pairs — rebuild when config changes
|
||||||
|
let _revealPairsCache = null;
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.data?.type === 'ss:config-updated') _revealPairsCache = null;
|
||||||
|
});
|
||||||
|
|
||||||
function revealText(text) {
|
function revealText(text) {
|
||||||
const pairs = buildRevealPairs();
|
if (!_revealPairsCache) _revealPairsCache = buildRevealPairs();
|
||||||
|
const pairs = _revealPairsCache;
|
||||||
let result = text;
|
let result = text;
|
||||||
for (const p of pairs) {
|
for (const p of pairs) {
|
||||||
const escaped = esc(p.from);
|
const escaped = esc(p.from);
|
||||||
@@ -579,13 +532,24 @@
|
|||||||
const originalTexts = new WeakMap();
|
const originalTexts = new WeakMap();
|
||||||
|
|
||||||
function revealInElement(el) {
|
function revealInElement(el) {
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
||||||
|
// Skip our own badge
|
||||||
|
if (el.classList?.contains('ss-reveal-badge')) return;
|
||||||
|
|
||||||
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode(node) {
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||||
|
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let textNode;
|
let textNode;
|
||||||
while ((textNode = walker.nextNode())) {
|
while ((textNode = walker.nextNode())) {
|
||||||
const text = textNode.textContent;
|
const text = textNode.textContent;
|
||||||
if (!text || text.trim().length === 0) continue;
|
if (!text || text.length < MIN_STRING_LENGTH) continue;
|
||||||
|
|
||||||
// Save original if not already saved
|
|
||||||
if (!originalTexts.has(textNode)) {
|
if (!originalTexts.has(textNode)) {
|
||||||
originalTexts.set(textNode, text);
|
originalTexts.set(textNode, text);
|
||||||
}
|
}
|
||||||
@@ -598,6 +562,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unrevealInElement(el) {
|
function unrevealInElement(el) {
|
||||||
|
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
||||||
|
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||||
let textNode;
|
let textNode;
|
||||||
while ((textNode = walker.nextNode())) {
|
while ((textNode = walker.nextNode())) {
|
||||||
@@ -608,54 +574,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reveal ALL existing responses on the page
|
// Elements to skip when revealing (inputs, scripts, styles, extension UI)
|
||||||
|
const SKIP_REVEAL_TAGS = new Set([
|
||||||
|
'SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'INPUT', 'TEXTAREA', 'SELECT',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Reveal ALL text on the page (not just specific selectors)
|
||||||
function revealAllResponses() {
|
function revealAllResponses() {
|
||||||
const elements = document.querySelectorAll(RESPONSE_SELECTORS);
|
revealInElement(document.body);
|
||||||
for (const el of elements) {
|
|
||||||
revealInElement(el);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Un-reveal ALL responses (restore originals)
|
// Un-reveal ALL text on the page
|
||||||
function unrevealAllResponses() {
|
function unrevealAllResponses() {
|
||||||
const elements = document.querySelectorAll(RESPONSE_SELECTORS);
|
unrevealInElement(document.body);
|
||||||
for (const el of elements) {
|
|
||||||
unrevealInElement(el);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for new content (streaming responses, new messages)
|
// Watch for ANY new content on the page
|
||||||
function observeResponses() {
|
function observeResponses() {
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
if (!settings.revealMode || !hasSubstitutions()) return;
|
if (!settings.revealMode || !hasSubstitutions()) return;
|
||||||
|
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
// Handle new nodes
|
// Handle new nodes — reveal all text in them
|
||||||
for (const node of mutation.addedNodes) {
|
for (const node of mutation.addedNodes) {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
if (!SKIP_REVEAL_TAGS.has(node.tagName)) {
|
||||||
const responseEls = node.querySelectorAll
|
revealInElement(node);
|
||||||
? node.querySelectorAll(RESPONSE_SELECTORS)
|
}
|
||||||
: [];
|
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||||
for (const el of responseEls) revealInElement(el);
|
const text = node.textContent;
|
||||||
|
if (text && text.length >= MIN_STRING_LENGTH) {
|
||||||
if (node.matches?.(RESPONSE_SELECTORS)) {
|
if (!originalTexts.has(node)) {
|
||||||
revealInElement(node);
|
originalTexts.set(node, text);
|
||||||
|
}
|
||||||
|
const revealed = revealText(text);
|
||||||
|
if (revealed !== text) {
|
||||||
|
node.textContent = revealed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle text changes in existing nodes (streaming)
|
// Handle text changes in existing nodes (streaming responses)
|
||||||
if (mutation.type === 'characterData' && settings.revealMode) {
|
if (mutation.type === 'characterData' && settings.revealMode) {
|
||||||
const parentEl = mutation.target.parentElement;
|
const text = mutation.target.textContent;
|
||||||
if (parentEl?.closest?.(RESPONSE_SELECTORS)) {
|
if (text && text.length >= MIN_STRING_LENGTH) {
|
||||||
const text = mutation.target.textContent;
|
const parent = mutation.target.parentElement;
|
||||||
const revealed = revealText(text);
|
if (parent && !SKIP_REVEAL_TAGS.has(parent.tagName)) {
|
||||||
if (revealed !== text) {
|
|
||||||
// Save original before overwriting
|
|
||||||
if (!originalTexts.has(mutation.target)) {
|
if (!originalTexts.has(mutation.target)) {
|
||||||
originalTexts.set(mutation.target, text);
|
originalTexts.set(mutation.target, text);
|
||||||
}
|
}
|
||||||
mutation.target.textContent = revealed;
|
const revealed = revealText(text);
|
||||||
|
if (revealed !== text) {
|
||||||
|
mutation.target.textContent = revealed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user