mirror of
https://github.com/vinta/awesome-python.git
synced 2026-10-20 18:52:21 +00:00
feat: add 5 new AgriAgent tools for web browsing and data export
screenshot_active_tab — captures current browser tab as JPEG; agent.js formats the result as an Anthropic vision image block so Claude can actually see the page (not just its text) get_page_content — reads text of the active tab via content script, or falls back to the reading-list cache when a URL is supplied export_farm_data — generates a CSV or JSON file and triggers a browser download for reading_list, field_profiles, ingested_files, or all data open_tab — opens any https:// URL in a new browser tab and waits for it to finish loading; returns tab_id for chained tool calls read_tab_content — extracts and parses page text from any tab by tab_id (or active tab) using chrome.scripting.executeScript; more reliable than the content-script sendMessage path Background worker gains CAPTURE_SCREENSHOT, GET_ACTIVE_TAB_CONTENT, OPEN_TAB, and READ_TAB_CONTENT message handlers; agent.js detects _type:'image' results and formats them as vision content blocks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBD2dN2KEjzz3UQFa9hEpu
This commit is contained in:
parent
3185b04ed6
commit
7346642563
4 changed files with 366 additions and 14 deletions
|
|
@ -86,11 +86,29 @@ export class AgrifineAgent {
|
||||||
|
|
||||||
this.onEvent({ type: 'tool_result', data: { name: block.name, result } });
|
this.onEvent({ type: 'tool_result', data: { name: block.name, result } });
|
||||||
|
|
||||||
toolResults.push({
|
// Screenshot tool returns an image — pass it as a vision content block
|
||||||
type: 'tool_result',
|
if (result && result._type === 'image') {
|
||||||
tool_use_id: block.id,
|
toolResults.push({
|
||||||
content: JSON.stringify(result),
|
type: 'tool_result',
|
||||||
});
|
tool_use_id: block.id,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
source: { type: 'base64', media_type: result.media_type, data: result.data },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Screenshot of "${result.title}" (${result.url})`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toolResults.push({
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: block.id,
|
||||||
|
content: JSON.stringify(result),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.push({ role: 'user', content: toolResults });
|
messages.push({ role: 'user', content: toolResults });
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
import { AgrifineAgent } from './agent.js';
|
import { AgrifineAgent } from './agent.js';
|
||||||
|
|
||||||
const TOOL_ICONS = {
|
const TOOL_ICONS = {
|
||||||
get_reading_list: '📖',
|
get_reading_list: '📖',
|
||||||
get_field_profiles: '🌱',
|
get_field_profiles: '🌱',
|
||||||
get_ingested_files: '📄',
|
get_ingested_files: '📄',
|
||||||
get_weather: '🌤️',
|
get_weather: '🌤️',
|
||||||
lookup_usda_soil: '🏛️',
|
lookup_usda_soil: '🏛️',
|
||||||
calculate_gdd: '📊',
|
calculate_gdd: '📊',
|
||||||
|
screenshot_active_tab: '📸',
|
||||||
|
get_page_content: '🔍',
|
||||||
|
export_farm_data: '⬇️',
|
||||||
|
open_tab: '🌐',
|
||||||
|
read_tab_content: '📋',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUGGESTED_PROMPTS = [
|
const SUGGESTED_PROMPTS = [
|
||||||
'What are my current field conditions and harvest windows?',
|
'What are my current field conditions and harvest windows?',
|
||||||
'Which fields have the best soil for carbon sequestration?',
|
'Which fields have the best soil for carbon sequestration?',
|
||||||
'Summarise all my farm data and flag any issues',
|
'Summarise all my farm data and flag any issues',
|
||||||
'What does the 7-day weather look like for my fields?',
|
'Screenshot this page and tell me what agricultural data you see',
|
||||||
'What USDA programs might I qualify for based on my fields?',
|
'Read this page and save any farm data you find',
|
||||||
|
'Export my reading list and field profiles to CSV',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AgRefineModule() {
|
export function AgRefineModule() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { getReadingList, getIngestedFiles, getFieldProfiles } from '../utils/storage.js';
|
import { getReadingList, getIngestedFiles, getFieldProfiles } from '../utils/storage.js';
|
||||||
|
|
||||||
|
function csvEscape(val) {
|
||||||
|
const s = String(val ?? '');
|
||||||
|
return (s.includes(',') || s.includes('"') || s.includes('\n'))
|
||||||
|
? `"${s.replace(/"/g, '""')}"` : s;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tool definitions sent to Claude ──────────────────────────────────────────
|
// ── Tool definitions sent to Claude ──────────────────────────────────────────
|
||||||
|
|
||||||
export const TOOL_DEFINITIONS = [
|
export const TOOL_DEFINITIONS = [
|
||||||
|
|
@ -86,6 +92,86 @@ export const TOOL_DEFINITIONS = [
|
||||||
required: ['latitude', 'longitude'],
|
required: ['latitude', 'longitude'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'screenshot_active_tab',
|
||||||
|
description: 'Take a screenshot of the currently active browser tab. Returns an image Claude can visually inspect — use this to see what the user is currently viewing, check a web page layout, verify data on screen, or analyse any visible content.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional note about why the screenshot is being taken (for context)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_page_content',
|
||||||
|
description: 'Fetch the full text content of the currently active browser tab, or look up a saved reading-list URL. Use this to read articles, extract data from web pages, or analyse the text of any page the user has open.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional URL to look up in the saved reading list. If omitted, reads the active tab.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'export_farm_data',
|
||||||
|
description: 'Generate and download a CSV or JSON export of farm data. Triggers a file download in the user\'s browser. Use when the user asks to export, download, or save their farm data.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
data_type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['reading_list', 'field_profiles', 'ingested_files', 'all'],
|
||||||
|
description: 'Which data set to export',
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['csv', 'json'],
|
||||||
|
description: 'File format (csv or json). "all" data_type always uses json.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['data_type'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'open_tab',
|
||||||
|
description: 'Open a URL in a new browser tab and wait for it to load. Use this to navigate to a relevant website — USDA, weather services, commodity markets, farm news, etc. After opening, call read_tab_content or screenshot_active_tab to extract information.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The full URL to open (must start with https:// or http://)',
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Why you are opening this URL — shown to the user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['url'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'read_tab_content',
|
||||||
|
description: 'Extract and parse the text content of a browser tab. Call after open_tab to read the page that was just loaded, or omit tab_id to read the currently active tab.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
tab_id: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Tab ID returned by open_tab. Omit to read the currently active tab.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'calculate_gdd',
|
name: 'calculate_gdd',
|
||||||
description: 'Calculate Growing Degree Days from temperature data. Uses base temp of 50°F for forage crops.',
|
description: 'Calculate Growing Degree Days from temperature data. Uses base temp of 50°F for forage crops.',
|
||||||
|
|
@ -128,6 +214,16 @@ export async function executeTool(name, input) {
|
||||||
return toolLookupUSDAsoil(input);
|
return toolLookupUSDAsoil(input);
|
||||||
case 'calculate_gdd':
|
case 'calculate_gdd':
|
||||||
return toolCalculateGDD(input);
|
return toolCalculateGDD(input);
|
||||||
|
case 'screenshot_active_tab':
|
||||||
|
return toolScreenshotActiveTab(input);
|
||||||
|
case 'get_page_content':
|
||||||
|
return toolGetPageContent(input);
|
||||||
|
case 'export_farm_data':
|
||||||
|
return toolExportFarmData(input);
|
||||||
|
case 'open_tab':
|
||||||
|
return toolOpenTab(input);
|
||||||
|
case 'read_tab_content':
|
||||||
|
return toolReadTabContent(input);
|
||||||
default:
|
default:
|
||||||
return { error: `Unknown tool: ${name}` };
|
return { error: `Unknown tool: ${name}` };
|
||||||
}
|
}
|
||||||
|
|
@ -284,6 +380,146 @@ async function toolLookupUSDAsoil({ latitude, longitude }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toolScreenshotActiveTab() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({ type: 'CAPTURE_SCREENSHOT' }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response?.error) {
|
||||||
|
reject(new Error(response.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Strip data URL prefix — agent.js will format this as an image content block
|
||||||
|
const base64 = response.dataUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||||
|
resolve({
|
||||||
|
_type: 'image',
|
||||||
|
media_type: 'image/jpeg',
|
||||||
|
data: base64,
|
||||||
|
url: response.url,
|
||||||
|
title: response.title,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toolGetPageContent({ url } = {}) {
|
||||||
|
// Check reading list cache first if a URL was given
|
||||||
|
if (url) {
|
||||||
|
const list = await getReadingList();
|
||||||
|
const saved = list.find((i) => i.url === url || i.url.startsWith(url));
|
||||||
|
if (saved) {
|
||||||
|
return {
|
||||||
|
url: saved.url,
|
||||||
|
title: saved.title,
|
||||||
|
summary: saved.summary,
|
||||||
|
tags: saved.tags,
|
||||||
|
source: 'reading_list_cache',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to reading the active tab via content script
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({ type: 'GET_ACTIVE_TAB_CONTENT' }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response?.error) {
|
||||||
|
reject(new Error(response.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toolExportFarmData({ data_type, format = 'csv' }) {
|
||||||
|
let records;
|
||||||
|
let filename;
|
||||||
|
let content;
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
if (data_type === 'all') {
|
||||||
|
const [rl, files, profiles] = await Promise.all([getReadingList(), getIngestedFiles(), getFieldProfiles()]);
|
||||||
|
filename = `agrifine_export_${date}.json`;
|
||||||
|
content = JSON.stringify({ reading_list: rl, ingested_files: files, field_profiles: profiles }, null, 2);
|
||||||
|
records = rl.length + files.length + profiles.length;
|
||||||
|
} else if (data_type === 'reading_list') {
|
||||||
|
const list = await getReadingList();
|
||||||
|
records = list.length;
|
||||||
|
filename = `agrifine_reading_list_${date}.${format}`;
|
||||||
|
if (format === 'json') {
|
||||||
|
content = JSON.stringify(list, null, 2);
|
||||||
|
} else {
|
||||||
|
const hdrs = ['title', 'url', 'summary', 'tags', 'savedAt'];
|
||||||
|
const rows = list.map((i) => [i.title, i.url, i.summary ?? '', (i.tags ?? []).join('; '), i.savedAt].map(csvEscape));
|
||||||
|
content = [hdrs.join(','), ...rows.map((r) => r.join(','))].join('\n');
|
||||||
|
}
|
||||||
|
} else if (data_type === 'field_profiles') {
|
||||||
|
const profiles = await getFieldProfiles();
|
||||||
|
records = profiles.length;
|
||||||
|
filename = `agrifine_field_profiles_${date}.${format}`;
|
||||||
|
if (format === 'json') {
|
||||||
|
content = JSON.stringify(profiles, null, 2);
|
||||||
|
} else {
|
||||||
|
const hdrs = ['name', 'acres', 'soil_type', 'latitude', 'longitude', 'clu_id', 'notes', 'created_at'];
|
||||||
|
const rows = profiles.map((p) => [
|
||||||
|
p.name, p.acres ?? '', p.soilType ?? '',
|
||||||
|
p.coordinates?.lat ?? '', p.coordinates?.lon ?? '',
|
||||||
|
p.cluId ?? '', p.notes ?? '', p.createdAt,
|
||||||
|
].map(csvEscape));
|
||||||
|
content = [hdrs.join(','), ...rows.map((r) => r.join(','))].join('\n');
|
||||||
|
}
|
||||||
|
} else if (data_type === 'ingested_files') {
|
||||||
|
const files = await getIngestedFiles();
|
||||||
|
records = files.length;
|
||||||
|
filename = `agrifine_ingested_files_${date}.json`;
|
||||||
|
content = JSON.stringify(files, null, 2);
|
||||||
|
} else {
|
||||||
|
return { error: `Unknown data_type: ${data_type}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger download inside the sidebar page
|
||||||
|
const mimeType = filename.endsWith('.json') ? 'application/json' : 'text/csv';
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = objectUrl;
|
||||||
|
anchor.download = filename;
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
document.body.removeChild(anchor);
|
||||||
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 2000);
|
||||||
|
|
||||||
|
return { exported: true, filename, record_count: records, format: filename.split('.').pop(), data_type };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolOpenTab({ url, reason }) {
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
return Promise.resolve({ error: 'URL must start with http:// or https://' });
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({ type: 'OPEN_TAB', payload: { url } }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
|
||||||
|
if (response?.error) { reject(new Error(response.error)); return; }
|
||||||
|
resolve({ ...response, reason: reason ?? null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolReadTabContent({ tab_id } = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({ type: 'READ_TAB_CONTENT', payload: { tab_id: tab_id ?? null } }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
|
||||||
|
if (response?.error) { reject(new Error(response.error)); return; }
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toolCalculateGDD({ daily_highs, daily_lows, base_temp = 50 }) {
|
function toolCalculateGDD({ daily_highs, daily_lows, base_temp = 50 }) {
|
||||||
const gdd_per_day = daily_highs.map((hi, i) => {
|
const gdd_per_day = daily_highs.map((hi, i) => {
|
||||||
const lo = daily_lows[i] ?? hi;
|
const lo = daily_lows[i] ?? hi;
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,33 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case 'GET_PAGE_CONTENT':
|
case 'GET_PAGE_CONTENT':
|
||||||
// Content script relays page text; background stores it temporarily
|
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
case 'CAPTURE_SCREENSHOT':
|
||||||
|
captureActiveTabScreenshot()
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((err) => sendResponse({ error: err.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'GET_ACTIVE_TAB_CONTENT':
|
||||||
|
getActiveTabContent()
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((err) => sendResponse({ error: err.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'OPEN_TAB':
|
||||||
|
openUrlInTab(message.payload.url)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((err) => sendResponse({ error: err.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'READ_TAB_CONTENT':
|
||||||
|
readTabContent(message.payload?.tab_id)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((err) => sendResponse({ error: err.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +57,75 @@ async function handleAnthropicRequest({ system, userMessage, maxTokens }) {
|
||||||
return { text };
|
return { text };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function captureActiveTabScreenshot() {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab) throw new Error('No active tab found');
|
||||||
|
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'jpeg', quality: 80 });
|
||||||
|
return { dataUrl, url: tab.url, title: tab.title };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getActiveTabContent() {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab) throw new Error('No active tab found');
|
||||||
|
try {
|
||||||
|
const resp = await chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_INFO' });
|
||||||
|
return { url: tab.url, title: tab.title, text: resp?.text ?? '', source: 'active_tab' };
|
||||||
|
} catch (_) {
|
||||||
|
return {
|
||||||
|
url: tab.url,
|
||||||
|
title: tab.title,
|
||||||
|
text: '',
|
||||||
|
source: 'active_tab',
|
||||||
|
note: 'Content script unavailable on this page (chrome://, extensions, etc.)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForTabLoad(tabId, timeoutMs = 20000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
chrome.tabs.onUpdated.removeListener(listener);
|
||||||
|
chrome.tabs.get(tabId).then(resolve).catch(() => reject(new Error('Tab load timed out')));
|
||||||
|
}, timeoutMs);
|
||||||
|
function listener(id, info, tab) {
|
||||||
|
if (id !== tabId || info.status !== 'complete') return;
|
||||||
|
chrome.tabs.onUpdated.removeListener(listener);
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(tab);
|
||||||
|
}
|
||||||
|
chrome.tabs.onUpdated.addListener(listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openUrlInTab(url) {
|
||||||
|
const tab = await chrome.tabs.create({ url, active: true });
|
||||||
|
const loaded = await waitForTabLoad(tab.id);
|
||||||
|
return { tab_id: loaded.id, url: loaded.url, title: loaded.title, status: 'ready' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readTabContent(tabId) {
|
||||||
|
const targetId = tabId
|
||||||
|
?? (await chrome.tabs.query({ active: true, currentWindow: true }))[0]?.id;
|
||||||
|
if (!targetId) throw new Error('No tab found');
|
||||||
|
|
||||||
|
const [result] = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: targetId },
|
||||||
|
func: () => {
|
||||||
|
const selectors = ['article', 'main', '[role="main"]', '.content', '#content'];
|
||||||
|
for (const sel of selectors) {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (el) {
|
||||||
|
const clone = el.cloneNode(true);
|
||||||
|
clone.querySelectorAll('script,style,nav,header,footer,aside').forEach((n) => n.remove());
|
||||||
|
return { url: location.href, title: document.title, text: clone.innerText.trim().slice(0, 8000) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { url: location.href, title: document.title, text: document.body?.innerText?.slice(0, 8000) ?? '' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return result.result;
|
||||||
|
}
|
||||||
|
|
||||||
// Keep service worker alive during active side-panel sessions
|
// Keep service worker alive during active side-panel sessions
|
||||||
chrome.runtime.onConnect.addListener((port) => {
|
chrome.runtime.onConnect.addListener((port) => {
|
||||||
if (port.name === 'keepalive') {
|
if (port.name === 'keepalive') {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue