A browser extension that lets you summarize any webpage and ask questions using AI.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

longer summary context and indicator when not full context

+230 -104
+19
popup/popup.css
··· 229 229 230 230 .result.hidden { display: none; } 231 231 232 + /* ── Truncation Warning ── */ 233 + .truncation-warning { 234 + display: flex; 235 + align-items: center; 236 + gap: 8px; 237 + padding: 10px 12px; 238 + margin-bottom: 16px; 239 + background: var(--bg-subtle); 240 + border: 1px solid var(--border); 241 + border-radius: 6px; 242 + font-size: 12px; 243 + color: var(--text-muted); 244 + } 245 + 246 + .truncation-warning svg { 247 + flex-shrink: 0; 248 + color: var(--text-muted); 249 + } 250 + 232 251 @keyframes fadeUp { 233 252 from { opacity: 0; transform: translateY(3px); } 234 253 to { opacity: 1; transform: translateY(0); }
+155 -91
popup/popup.js
··· 9 9 let currentTabUrl = ""; 10 10 let chatHistory = []; // Array of {role: "user" | "assistant", content: string} 11 11 let isChatLoading = false; 12 + let contentWasTruncated = false; // Track if content was truncated during extraction 12 13 13 14 const resultContainer = document.getElementById("result"); 14 15 const initialState = document.getElementById("initial-state"); ··· 26 27 const DETAILED_SUMMARY_CACHE_PREFIX = "detailed_summary_cache_"; 27 28 const CONTENT_CACHE_PREFIX = "content_cache_"; 28 29 const CHAT_CACHE_PREFIX = "chat_cache_"; 30 + const TRUNCATION_CACHE_PREFIX = "truncation_cache_"; 31 + 32 + // ── Prompts ────────────────────────────────────────────────── 33 + const SYSTEM_PROMPT_SUMMARIZER = 34 + "You are a helpful assistant that summarizes webpages concisely."; 35 + const SYSTEM_PROMPT_CHAT = 36 + "You are a helpful assistant answering questions about a webpage. Use the provided page content and summary to give accurate, concise answers. You may use short sentences, sections, and bullet points to answer. Avoid long paragraphs and tables. ONLY answer based on the provided page content and summary, not any external knowledge or information."; 37 + const QUICK_SUMMARY_PROMPT = `Please provide a "Quick Summary" of this webpage. Focus on the main points and key takeaways. Use markdown formatting (headings, bullet points, etc.). 38 + 39 + The "Quick Summary" should be 3-5 **short** one-sentence bullet points. Each of these bullet points should have key points/takeaways **bolded** so people can quickly scan.`; 29 40 30 41 // ── Theme logic ────────────────────────────────────────────── 31 42 // Cycles: light → dark → system ··· 42 53 } 43 54 44 55 // Update icon visibility 45 - document.getElementById("theme-icon-light").classList.toggle("hidden", theme !== "light"); 46 - document.getElementById("theme-icon-dark").classList.toggle("hidden", theme !== "dark"); 47 - document.getElementById("theme-icon-system").classList.toggle("hidden", theme !== "system"); 56 + document 57 + .getElementById("theme-icon-light") 58 + .classList.toggle("hidden", theme !== "light"); 59 + document 60 + .getElementById("theme-icon-dark") 61 + .classList.toggle("hidden", theme !== "dark"); 62 + document 63 + .getElementById("theme-icon-system") 64 + .classList.toggle("hidden", theme !== "system"); 48 65 49 66 // Update tooltip 50 - const labels = { light: "Light mode", dark: "Dark mode", system: "System theme" }; 67 + const labels = { 68 + light: "Light mode", 69 + dark: "Dark mode", 70 + system: "System theme", 71 + }; 51 72 themeBtn.title = labels[theme]; 52 73 } 53 74 ··· 61 82 }); 62 83 63 84 // Re-apply when system preference changes (for "system" mode) 64 - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { 65 - if (currentTheme === "system") applyTheme("system"); 66 - }); 85 + window 86 + .matchMedia("(prefers-color-scheme: dark)") 87 + .addEventListener("change", () => { 88 + if (currentTheme === "system") applyTheme("system"); 89 + }); 67 90 68 91 function setSummarizeLabel(text) { 69 92 summarizeLabel.textContent = text; ··· 84 107 QUICK_SUMMARY_CACHE_PREFIX + currentTabId, 85 108 DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 86 109 CONTENT_CACHE_PREFIX + currentTabId, 87 - CHAT_CACHE_PREFIX + currentTabId 110 + CHAT_CACHE_PREFIX + currentTabId, 88 111 ]); 89 112 90 113 const cachedQuickSummary = cached[QUICK_SUMMARY_CACHE_PREFIX + currentTabId]; 91 - const cachedDetailedSummary = cached[DETAILED_SUMMARY_CACHE_PREFIX + currentTabId]; 114 + const cachedDetailedSummary = 115 + cached[DETAILED_SUMMARY_CACHE_PREFIX + currentTabId]; 92 116 const cachedContent = cached[CONTENT_CACHE_PREFIX + currentTabId]; 93 117 const cachedChat = cached[CHAT_CACHE_PREFIX + currentTabId]; 94 118 ··· 96 120 summarizeBtn.disabled = true; 97 121 setSummarizeLabel("Loading..."); 98 122 99 - // If we have cached content for this tab, restore it 100 - if (cachedContent && cachedContent.url === currentTabUrl) { 101 - currentPageContent = cachedContent.content; 102 - isExtracting = false; 103 - summarizeBtn.disabled = false; 123 + // Check for truncation cache 124 + const cachedTruncation = await chrome.storage.session.get([ 125 + TRUNCATION_CACHE_PREFIX + currentTabId, 126 + ]); 104 127 105 - // Restore summaries if they exist 106 - if (cachedQuickSummary) { 107 - quickSummary = cachedQuickSummary.summary; 108 - } 109 - if (cachedDetailedSummary) { 110 - detailedSummary = cachedDetailedSummary.summary; 111 - } 112 - // Restore chat history if it exists 113 - if (cachedChat && cachedChat.messages) { 114 - chatHistory = cachedChat.messages; 115 - } 128 + // If we have cached content for this tab, restore it 129 + if (cachedContent && cachedContent.url === currentTabUrl) { 130 + currentPageContent = cachedContent.content; 131 + contentWasTruncated = cachedContent.wasTruncated || false; 132 + isExtracting = false; 133 + summarizeBtn.disabled = false; 116 134 117 - // Build combined display if we have any summaries 118 - if (quickSummary || detailedSummary) { 119 - let combinedContent = ""; 120 - if (quickSummary) { 121 - combinedContent += quickSummary; 135 + // Restore summaries if they exist 136 + if (cachedQuickSummary) { 137 + quickSummary = cachedQuickSummary.summary; 138 + } 139 + if (cachedDetailedSummary) { 140 + detailedSummary = cachedDetailedSummary.summary; 141 + } 142 + // Restore chat history if it exists 143 + if (cachedChat && cachedChat.messages) { 144 + chatHistory = cachedChat.messages; 122 145 } 123 - if (detailedSummary) { 124 - if (combinedContent) combinedContent += "\n\n---\n\n"; 125 - combinedContent += detailedSummary; 146 + 147 + // Build combined display if we have any summaries 148 + if (quickSummary || detailedSummary) { 149 + let combinedContent = ""; 150 + if (quickSummary) { 151 + combinedContent += quickSummary; 152 + } 153 + if (detailedSummary) { 154 + if (combinedContent) combinedContent += "\n\n---\n\n"; 155 + combinedContent += detailedSummary; 156 + } 157 + showSummary(combinedContent); 158 + setSummarizeLabel("Regenerate"); 159 + // Show chat if we have a summary and chat history 160 + showChat(); 161 + renderChatMessages(); 162 + } else { 163 + // No summaries yet 164 + setSummarizeLabel("Quick Summary"); 126 165 } 127 - showSummary(combinedContent); 128 - setSummarizeLabel("Regenerate"); 129 - // Show chat if we have a summary and chat history 130 - showChat(); 131 - renderChatMessages(); 132 166 } else { 133 - // No summaries yet 167 + // No cache, extract fresh content 168 + await extractPageContent(); 169 + isExtracting = false; 170 + summarizeBtn.disabled = false; 134 171 setSummarizeLabel("Quick Summary"); 135 - } 136 - } else { 137 - // No cache, extract fresh content 138 - await extractPageContent(); 139 - isExtracting = false; 140 - summarizeBtn.disabled = false; 141 - setSummarizeLabel("Quick Summary"); 142 172 143 - // Check if we should auto-trigger summarize (from context menu) 144 - const session = await chrome.storage.session.get(["triggerSummarize"]); 145 - if (session.triggerSummarize) { 146 - // Clear the flag 147 - await chrome.storage.session.remove("triggerSummarize"); 148 - // Trigger quick summarize 149 - if (!isLoading && !isExtracting && currentPageContent) { 150 - await generateQuickSummary(); 173 + // Check if we should auto-trigger summarize (from context menu) 174 + const session = await chrome.storage.session.get(["triggerSummarize"]); 175 + if (session.triggerSummarize) { 176 + // Clear the flag 177 + await chrome.storage.session.remove("triggerSummarize"); 178 + // Trigger quick summarize 179 + if (!isLoading && !isExtracting && currentPageContent) { 180 + await generateQuickSummary(); 181 + } 151 182 } 152 183 } 153 - } 154 184 }); 155 185 156 186 function resetUI() { ··· 158 188 detailedSummary = ""; 159 189 currentSummaryMode = "none"; 160 190 chatHistory = []; 191 + contentWasTruncated = false; 161 192 resultContainer.innerHTML = ""; 162 193 resultContainer.classList.add("hidden"); 163 194 initialState.classList.remove("hidden"); ··· 187 218 await chrome.storage.session.remove([ 188 219 QUICK_SUMMARY_CACHE_PREFIX + currentTabId, 189 220 DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 190 - CHAT_CACHE_PREFIX + currentTabId 221 + CHAT_CACHE_PREFIX + currentTabId, 191 222 ]); 192 223 quickSummary = ""; 193 224 detailedSummary = ""; ··· 209 240 tab.url.startsWith("edge://") 210 241 ) { 211 242 currentPageContent = ""; 243 + contentWasTruncated = false; 212 244 return; 213 245 } 214 246 const response = await chrome.tabs.sendMessage(tab.id, { ··· 216 248 }); 217 249 if (response && response.content) { 218 250 currentPageContent = response.content; 251 + contentWasTruncated = response.wasTruncated || false; 252 + // Cache the content with truncation info 253 + if (currentTabId) { 254 + await chrome.storage.session.set({ 255 + [CONTENT_CACHE_PREFIX + currentTabId]: { 256 + content: currentPageContent, 257 + wasTruncated: contentWasTruncated, 258 + url: currentTabUrl, 259 + }, 260 + }); 261 + } 219 262 } 220 263 } catch (error) { 221 264 console.error("Error extracting content:", error); 222 265 currentPageContent = ""; 266 + contentWasTruncated = false; 223 267 } 224 268 } 225 269 ··· 234 278 function showSummary(content) { 235 279 initialState.classList.add("hidden"); 236 280 resultContainer.classList.remove("hidden"); 237 - resultContainer.innerHTML = renderMarkdown(content); 281 + 282 + // Build the HTML content 283 + let htmlContent = ""; 284 + 285 + // Add truncation warning if applicable 286 + if (contentWasTruncated) { 287 + htmlContent += ` 288 + <div class="truncation-warning"> 289 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 290 + <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path> 291 + <line x1="12" y1="9" x2="12" y2="13"></line> 292 + <line x1="12" y1="17" x2="12.01" y2="17"></line> 293 + </svg> 294 + <span>Partial summary based on first part of article</span> 295 + </div> 296 + `; 297 + } 298 + 299 + htmlContent += renderMarkdown(content); 300 + resultContainer.innerHTML = htmlContent; 238 301 239 302 // Create button container for copy and regenerate 240 303 const buttonContainer = document.createElement("div"); ··· 311 374 await chrome.storage.session.remove([ 312 375 QUICK_SUMMARY_CACHE_PREFIX + currentTabId, 313 376 DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 314 - CHAT_CACHE_PREFIX + currentTabId 377 + CHAT_CACHE_PREFIX + currentTabId, 315 378 ]); 316 379 quickSummary = ""; 317 380 detailedSummary = ""; ··· 338 401 if (chatHistory.length === 0) return; 339 402 340 403 // Check if we already rendered chat messages (look for chat-divider) 341 - let chatSection = resultContainer.querySelector('.chat-section'); 404 + let chatSection = resultContainer.querySelector(".chat-section"); 342 405 343 406 if (!chatSection) { 344 407 // First time rendering chat - add divider and section 345 - chatSection = document.createElement('div'); 346 - chatSection.className = 'chat-section'; 408 + chatSection = document.createElement("div"); 409 + chatSection.className = "chat-section"; 347 410 348 - const divider = document.createElement('hr'); 349 - divider.className = 'chat-divider'; 411 + const divider = document.createElement("hr"); 412 + divider.className = "chat-divider"; 350 413 chatSection.appendChild(divider); 351 414 352 415 resultContainer.appendChild(chatSection); ··· 354 417 355 418 // Clear existing messages (we'll re-render all) 356 419 // Keep only the divider 357 - const divider = chatSection.querySelector('.chat-divider'); 358 - chatSection.innerHTML = ''; 420 + const divider = chatSection.querySelector(".chat-divider"); 421 + chatSection.innerHTML = ""; 359 422 chatSection.appendChild(divider); 360 423 361 424 // Render all messages ··· 367 430 }); 368 431 369 432 // Scroll to bottom of content-container (not just messages) 370 - const contentContainer = document.querySelector('.content-container'); 433 + const contentContainer = document.querySelector(".content-container"); 371 434 contentContainer.scrollTop = contentContainer.scrollHeight; 372 435 } 373 436 ··· 385 448 chatSendBtn.disabled = true; 386 449 387 450 // Find chat section or create it 388 - let chatSection = resultContainer.querySelector('.chat-section'); 451 + let chatSection = resultContainer.querySelector(".chat-section"); 389 452 if (!chatSection) { 390 - chatSection = document.createElement('div'); 391 - chatSection.className = 'chat-section'; 392 - const divider = document.createElement('hr'); 393 - divider.className = 'chat-divider'; 453 + chatSection = document.createElement("div"); 454 + chatSection.className = "chat-section"; 455 + const divider = document.createElement("hr"); 456 + divider.className = "chat-divider"; 394 457 chatSection.appendChild(divider); 395 458 resultContainer.appendChild(chatSection); 396 459 } ··· 401 464 chatSection.appendChild(loadingEl); 402 465 403 466 // Scroll to show loading 404 - const contentContainer = document.querySelector('.content-container'); 467 + const contentContainer = document.querySelector(".content-container"); 405 468 contentContainer.scrollTop = contentContainer.scrollHeight; 406 469 407 470 try { ··· 412 475 apiKey: "", 413 476 }); 414 477 415 - const pageContentForLLM = currentPageContent.substring(0, 6000); 416 - const summaryContent = quickSummary + (detailedSummary ? "\n\n" + detailedSummary : ""); 478 + // Increased context limit for LLM (was 6000, now 12000) 479 + const pageContentForLLM = currentPageContent.substring(0, 12000); 480 + const summaryContent = 481 + quickSummary + (detailedSummary ? "\n\n" + detailedSummary : ""); 417 482 418 483 // Build messages for API - include recent chat history (last 4 messages for context) 419 484 const recentHistory = chatHistory.slice(-5, -1); // Exclude the message we just added 420 485 const apiMessages = [ 421 - { role: "system", content: "You are a helpful assistant answering questions about a webpage. Use the provided page content and summary to give accurate, concise answers." }, 486 + { role: "system", content: SYSTEM_PROMPT_CHAT }, 422 487 { role: "system", content: `Page content:\n\n${pageContentForLLM}` }, 423 488 { role: "system", content: `Summary of the page:\n\n${summaryContent}` }, 424 - ...recentHistory.map(m => ({ role: m.role, content: m.content })), 425 - { role: "user", content: message } 489 + ...recentHistory.map((m) => ({ role: m.role, content: m.content })), 490 + { role: "user", content: message }, 426 491 ]; 427 492 428 493 const response = await chrome.runtime.sendMessage({ ··· 459 524 [CHAT_CACHE_PREFIX + currentTabId]: { 460 525 messages: chatHistory, 461 526 url: currentTabUrl, 462 - timestamp: Date.now() 463 - } 527 + timestamp: Date.now(), 528 + }, 464 529 }); 465 530 } 466 531 } catch (error) { ··· 475 540 chatSection.appendChild(errorEl); 476 541 477 542 // Scroll to show error 478 - const contentContainer = document.querySelector('.content-container'); 543 + const contentContainer = document.querySelector(".content-container"); 479 544 contentContainer.scrollTop = contentContainer.scrollHeight; 480 545 } finally { 481 546 isChatLoading = false; ··· 513 578 apiKey: "", 514 579 }); 515 580 516 - const pageContentForLLM = currentPageContent.substring(0, 8000); 517 - 518 - const quickSummaryPrompt = `Please provide a "Quick Summary" of this webpage. Focus on the main points and key takeaways. Use markdown formatting (headings, bullet points, etc.). 519 - 520 - The "Quick Summary" should be 3-5 **short** one-sentence bullet points. Each of these bullet points should have key points/takeaways **bolded** so people can quickly scan.`; 581 + // Increased context limit for LLM (was 8000, now 15000) 582 + const pageContentForLLM = currentPageContent.substring(0, 15000); 521 583 522 584 const apiMessages = [ 523 - { role: "system", content: "You are a helpful assistant that summarizes webpages concisely." }, 585 + { role: "system", content: SYSTEM_PROMPT_SUMMARIZER }, 524 586 { 525 587 role: "system", 526 588 content: `The following is the content of the current webpage:\n\n${pageContentForLLM}`, 527 589 }, 528 590 { 529 591 role: "user", 530 - content: quickSummaryPrompt, 592 + content: QUICK_SUMMARY_PROMPT, 531 593 }, 532 594 ]; 533 595 ··· 565 627 [QUICK_SUMMARY_CACHE_PREFIX + currentTabId]: { 566 628 summary: quickSummary, 567 629 url: currentTabUrl, 568 - timestamp: Date.now() 630 + timestamp: Date.now(), 569 631 }, 570 632 [CONTENT_CACHE_PREFIX + currentTabId]: { 571 633 content: currentPageContent, 572 - url: currentTabUrl 573 - } 634 + url: currentTabUrl, 635 + }, 574 636 }); 575 637 // Clear any old detailed summary cache since we're regenerating 576 - await chrome.storage.session.remove([DETAILED_SUMMARY_CACHE_PREFIX + currentTabId]); 638 + await chrome.storage.session.remove([ 639 + DETAILED_SUMMARY_CACHE_PREFIX + currentTabId, 640 + ]); 577 641 } 578 642 } catch (error) { 579 643 console.error("API Error:", error);
+7 -1
scripts/background.js
··· 6 6 const CONTENT_CACHE_PREFIX = "content_cache_"; 7 7 const CHAT_CACHE_PREFIX = "chat_cache_"; 8 8 9 + // ── Prompt templates ───────────────────────────────────────── 10 + const OLLAMA_CONTEXT_TEMPLATE = "Context:\n${context}\n\nUser: ${userMessage}"; 11 + const OLLAMA_SINGLE_MESSAGE_TEMPLATE = "${userMessage}"; 12 + 9 13 chrome.runtime.onInstalled.addListener(() => { 10 14 // Set default settings only if they don't already exist 11 15 chrome.storage.sync.get(["apiMode"]).then((result) => { ··· 144 148 .slice(0, -1) 145 149 .map((m) => `${m.role}: ${m.content}`) 146 150 .join("\n"); 147 - prompt = `Context:\n${context}\n\nUser: ${lastUserMsg?.content || ""}`; 151 + prompt = OLLAMA_CONTEXT_TEMPLATE 152 + .replace("${context}", context) 153 + .replace("${userMessage}", lastUserMsg?.content || ""); 148 154 } else { 149 155 prompt = lastUserMsg?.content || ""; 150 156 }
+49 -12
scripts/content.js
··· 18 18 'nav', 'aside', 'form', 'button', 'input' 19 19 ]; 20 20 21 + // Maximum characters to extract (increased to capture more content) 22 + const MAX_EXTRACTION_LENGTH = 50000; 23 + 21 24 function extractText() { 22 25 // Extract text with structure 23 26 let extractedText = ''; 27 + let wasTruncated = false; 24 28 25 29 // Get title 26 30 const title = document.title || ''; ··· 37 41 // Extract content from body, skipping noise elements 38 42 extractedText += extractTextFromElement(document.body); 39 43 40 - // Clean up the text 41 - extractedText = cleanText(extractedText); 44 + // Check if content exceeds limit before cleaning 45 + const originalLength = extractedText.length; 46 + 47 + // Clean up the text (without truncating yet) 48 + extractedText = cleanText(extractedText, false); 49 + 50 + // Track if we need to truncate 51 + if (extractedText.length > MAX_EXTRACTION_LENGTH) { 52 + wasTruncated = true; 53 + extractedText = extractedText.substring(0, MAX_EXTRACTION_LENGTH); 54 + } 42 55 43 56 // Fallback: if we got very little content, try brute force extraction 44 57 if (extractedText.length < 1000) { 45 - const fallbackText = extractTextFallback(); 58 + const fallbackResult = extractTextFallback(); 59 + const fallbackText = fallbackResult.text; 46 60 if (fallbackText.length > extractedText.length) { 47 61 extractedText = `Title: ${title}\n\n${fallbackText}`; 62 + wasTruncated = fallbackResult.wasTruncated; 48 63 } 49 64 } 50 65 51 - return extractedText; 66 + return { text: extractedText, wasTruncated }; 52 67 } 53 68 54 69 function extractTextFallback() { ··· 61 76 ]; 62 77 63 78 let text = ''; 79 + let wasTruncated = false; 64 80 const seen = new Set(); 65 81 66 82 for (const selector of selectors) { ··· 77 93 78 94 seen.add(content.substring(0, 100)); 79 95 text += content + '\n\n'; 96 + 97 + // Check if we're approaching the limit 98 + if (text.length > MAX_EXTRACTION_LENGTH) { 99 + wasTruncated = true; 100 + break; 101 + } 80 102 } 81 103 } catch (e) { 82 104 // Ignore invalid selectors 83 105 } 106 + if (wasTruncated) break; 84 107 } 85 108 86 109 // Last resort: get all paragraphs on the page 87 - if (text.length < 500) { 110 + if (text.length < 500 && !wasTruncated) { 88 111 const allParagraphs = document.querySelectorAll('p'); 89 112 for (const p of allParagraphs) { 90 113 const content = p.textContent.trim(); ··· 94 117 95 118 seen.add(content.substring(0, 100)); 96 119 text += content + '\n\n'; 120 + 121 + if (text.length > MAX_EXTRACTION_LENGTH) { 122 + wasTruncated = true; 123 + break; 124 + } 97 125 } 98 126 } 99 127 } 100 128 101 - return text.substring(0, 15000); 129 + return { text: text.substring(0, MAX_EXTRACTION_LENGTH), wasTruncated }; 102 130 } 103 131 104 132 function shouldSkipElement(el) { ··· 296 324 return text; 297 325 } 298 326 299 - function cleanText(text) { 300 - return text 327 + function cleanText(text, shouldTruncate = true) { 328 + let cleaned = text 301 329 .replace(/[^\S\n]+/g, ' ') // Collapse spaces/tabs but preserve newlines 302 330 .replace(/\n{3,}/g, '\n\n') // Collapse 3+ newlines to 2 303 - .replace(/^\s+|\s+$/g, '') // Trim 304 - .substring(0, 15000); // Limit length 331 + .replace(/^\s+|\s+$/g, ''); // Trim 332 + 333 + // Only truncate if explicitly requested (used for final output) 334 + if (shouldTruncate && cleaned.length > MAX_EXTRACTION_LENGTH) { 335 + cleaned = cleaned.substring(0, MAX_EXTRACTION_LENGTH); 336 + } 337 + 338 + return cleaned; 305 339 } 306 340 307 341 // Listen for messages from popup 308 342 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 309 343 if (request.action === 'extract') { 310 - const content = extractText(); 311 - sendResponse({ content }); 344 + const result = extractText(); 345 + sendResponse({ 346 + content: result.text, 347 + wasTruncated: result.wasTruncated 348 + }); 312 349 } 313 350 return true; 314 351 });