diff --git a/AGENTS.md b/AGENTS.md index 401f962..9b94461 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ This file provides guidelines for codex agents contributing to the Sortana proje ## Testing -There are currently no automated tests for this project. If you add tests in the future, specify the commands to run them here. For now, verification must happen manually in Thunderbird. Do **not** run the `ps1` build script or the SVG processing script. +There are currently no automated tests for this project. If you add tests in the future, specify the commands to run them here. For now, verification must happen manually in Thunderbird. ## Documentation diff --git a/README.md b/README.md index 57a0285..7436436 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ message meets a specified criterion. - **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page. - **Markdown conversion** – optionally convert HTML bodies to Markdown before sending them to the AI service. - **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service. -- **Debug tab** – view the last request payload and a diff between the unaltered message text and the final prompt. - **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. - **Automatic rules** – create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. @@ -127,7 +126,6 @@ Sortana requests the following Thunderbird permissions: - `accountsRead` – list accounts and folders for move or copy actions. - `menus` – add context menu commands. - `tabs` – open new tabs and query the active tab. -- `notifications` – display error notifications. - `compose` – create reply and forward compose windows for matching rules. ## Thunderbird Add-on Store Disclosures @@ -141,8 +139,6 @@ uses the following third party libraries: - MIT License - [turndown v7.2.0](https://github.com/mixmark-io/turndown/tree/v7.2.0) - MIT License -- [diff](https://github.com/google/diff-match-patch/blob/62f2e689f498f9c92dbc588c58750addec9b1654/javascript/diff_match_patch_uncompressed.js) - - Apache-2.0 license ## License diff --git a/_locales/en-US/messages.json b/_locales/en-US/messages.json index 65181c8..983bb86 100644 --- a/_locales/en-US/messages.json +++ b/_locales/en-US/messages.json @@ -18,8 +18,7 @@ "options.htmlToMarkdown": { "message": "Convert HTML body to Markdown" }, "options.stripUrlParams": { "message": "Remove URL tracking parameters" }, "options.altTextImages": { "message": "Replace images with alt text" }, - "options.collapseWhitespace": { "message": "Collapse long whitespace" }, - "options.tokenReduction": { "message": "Aggressive token reduction" } + "options.collapseWhitespace": { "message": "Collapse long whitespace" } ,"action.read": { "message": "read" } ,"action.flag": { "message": "flag" } ,"action.copy": { "message": "copy" } diff --git a/ai-filter.sln b/ai-filter.sln index ea71f1b..57705eb 100644 --- a/ai-filter.sln +++ b/ai-filter.sln @@ -108,7 +108,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "img", "img", "{F266602F-175 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "js", "js", "{21D2A42C-3F85-465C-9141-C106AFD92B68}" ProjectSection(SolutionItems) = preProject - resources\js\diff_match_patch_uncompressed.js = resources\js\diff_match_patch_uncompressed.js resources\js\turndown.js = resources\js\turndown.js EndProjectSection EndProject diff --git a/background.js b/background.js index fc585ff..06683e2 100644 --- a/background.js +++ b/background.js @@ -26,14 +26,11 @@ let htmlToMarkdown = false; let stripUrlParams = false; let altTextImages = false; let collapseWhitespace = false; -let tokenReduction = false; -let maxTokens = 4096; let TurndownService = null; let userTheme = 'auto'; let currentTheme = 'light'; let detectSystemTheme; let errorPending = false; -let showDebugTab = false; const ERROR_NOTIFICATION_ID = 'sortana-error'; function normalizeRules(rules) { @@ -128,16 +125,12 @@ function byteSize(str) { } function replaceInlineBase64(text) { - return text.replace(/(?:data:[^;]+;base64,)?[A-Za-z0-9+/=\r\n]{100,}/g, - m => tokenReduction ? '__BASE64__' : `[base64: ${byteSize(m)} bytes]`); + return text.replace(/[A-Za-z0-9+/]{100,}={0,2}/g, + m => `[base64: ${byteSize(m)} bytes]`); } function sanitizeString(text) { let t = String(text); - if (tokenReduction) { - t = t.replace(//gs, '') - .replace(/url\([^\)]*\)/gi, 'url(__IMG__)'); - } if (stripUrlParams) { t = t.replace(/https?:\/\/[^\s)]+/g, m => { const idx = m.indexOf('?'); @@ -145,7 +138,7 @@ function sanitizeString(text) { }); } if (collapseWhitespace) { - t = t.replace(/[\u200B-\u200D\u2060\s]{2,}/g, ' ').replace(/\n{3,}/g, '\n\n'); + t = t.replace(/[ \t\u00A0]{2,}/g, ' ').replace(/\n{3,}/g, '\n\n'); } return t; } @@ -164,26 +157,12 @@ function collectText(part, bodyParts, attachments) { attachments.push(`${name} (${ct}, ${part.size || byteSize(body)} bytes)`); } else if (ct.startsWith("text/html")) { const doc = new DOMParser().parseFromString(body, 'text/html'); - if (tokenReduction) { - doc.querySelectorAll('script,style').forEach(el => el.remove()); - const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_COMMENT); - let node; - while ((node = walker.nextNode())) { - node.parentNode.removeChild(node); - } - doc.querySelectorAll('*').forEach(el => { - for (const attr of Array.from(el.attributes)) { - if (!['href','src','alt'].includes(attr.name)) { - el.removeAttribute(attr.name); - } - } + if (altTextImages) { + doc.querySelectorAll('img').forEach(img => { + const alt = img.getAttribute('alt') || ''; + img.replaceWith(doc.createTextNode(alt)); }); } - doc.querySelectorAll('img').forEach(img => { - const alt = img.getAttribute('alt') || ''; - const text = altTextImages ? alt : '__IMG__'; - img.replaceWith(doc.createTextNode(text)); - }); if (stripUrlParams) { doc.querySelectorAll('[href]').forEach(a => { const href = a.getAttribute('href'); @@ -210,46 +189,17 @@ function collectText(part, bodyParts, attachments) { } } -function collectRawText(part, bodyParts, attachments) { - if (part.parts && part.parts.length) { - for (const p of part.parts) collectRawText(p, bodyParts, attachments); - return; - } - const ct = (part.contentType || "text/plain").toLowerCase(); - const cd = (part.headers?.["content-disposition"]?.[0] || "").toLowerCase(); - const body = String(part.body || ""); - if (cd.includes("attachment") || !ct.startsWith("text/")) { - const nameMatch = /filename\s*=\s*"?([^";]+)/i.exec(cd) || /name\s*=\s*"?([^";]+)/i.exec(part.headers?.["content-type"]?.[0] || ""); - const name = nameMatch ? nameMatch[1] : ""; - attachments.push(`${name} (${ct}, ${part.size || byteSize(body)} bytes)`); - } else if (ct.startsWith("text/html")) { - const doc = new DOMParser().parseFromString(body, 'text/html'); - bodyParts.push(doc.body.textContent || ""); - } else { - bodyParts.push(body); - } -} - -function buildEmailText(full, applyTransforms = true) { +function buildEmailText(full) { const bodyParts = []; const attachments = []; - const collect = applyTransforms ? collectText : collectRawText; - collect(full, bodyParts, attachments); + collectText(full, bodyParts, attachments); const headers = Object.entries(full.headers || {}) .map(([k, v]) => `${k}: ${v.join(' ')}`) .join('\n'); const attachInfo = `Attachments: ${attachments.length}` + (attachments.length ? "\n" + attachments.map(a => ` - ${a}`).join('\n') : ""); - let combined = `${headers}\n${attachInfo}\n\n${bodyParts.join('\n')}`.trim(); - if (applyTransforms && tokenReduction) { - const seen = new Set(); - combined = combined.split('\n').filter(l => { - if (seen.has(l)) return false; - seen.add(l); - return true; - }).join('\n'); - } - return applyTransforms ? sanitizeString(combined) : combined; + const combined = `${headers}\n${attachInfo}\n\n${bodyParts.join('\n')}`.trim(); + return sanitizeString(combined); } function updateTimingStats(elapsed) { @@ -283,17 +233,7 @@ async function processMessage(id) { updateActionIcon(); try { const full = await messenger.messages.getFull(id); - const originalText = buildEmailText(full, false); - let text = buildEmailText(full); - if (tokenReduction && maxTokens > 0) { - const limit = Math.floor(maxTokens * 0.9); - if (text.length > limit) { - text = text.slice(0, limit); - } - } - if (showDebugTab) { - await storage.local.set({ lastFullText: originalText, lastPromptText: text }); - } + const text = buildEmailText(full); let hdr; let currentTags = []; let alreadyRead = false; @@ -451,7 +391,7 @@ async function clearCacheForMessages(idsInput) { } try { - const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "errorPending", "showDebugTab"]); + const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "aiRules", "theme", "errorPending"]); logger.setDebug(store.debugLogging); await AiClassifier.setConfig(store); userTheme = store.theme || 'auto'; @@ -461,12 +401,7 @@ async function clearCacheForMessages(idsInput) { stripUrlParams = store.stripUrlParams === true; altTextImages = store.altTextImages === true; collapseWhitespace = store.collapseWhitespace === true; - tokenReduction = store.tokenReduction === true; - if (store.aiParams && typeof store.aiParams.max_tokens !== 'undefined') { - maxTokens = parseInt(store.aiParams.max_tokens) || maxTokens; - } errorPending = store.errorPending === true; - showDebugTab = store.showDebugTab === true; const savedStats = await storage.local.get('classifyStats'); if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') { Object.assign(timingStats, savedStats.classifyStats); @@ -482,25 +417,6 @@ async function clearCacheForMessages(idsInput) { aiRules = normalizeRules(newRules); logger.aiLog("aiRules updated from storage change", { debug: true }, aiRules); } - if (changes.endpoint || changes.templateName || changes.customTemplate || changes.customSystemPrompt || changes.aiParams || changes.debugLogging) { - const config = {}; - if (changes.endpoint) config.endpoint = changes.endpoint.newValue; - if (changes.templateName) config.templateName = changes.templateName.newValue; - if (changes.customTemplate) config.customTemplate = changes.customTemplate.newValue; - if (changes.customSystemPrompt) config.customSystemPrompt = changes.customSystemPrompt.newValue; - if (changes.aiParams) { - config.aiParams = changes.aiParams.newValue; - if (changes.aiParams.newValue && typeof changes.aiParams.newValue.max_tokens !== 'undefined') { - maxTokens = parseInt(changes.aiParams.newValue.max_tokens) || maxTokens; - } - } - if (changes.debugLogging) { - config.debugLogging = changes.debugLogging.newValue === true; - logger.setDebug(config.debugLogging); - } - await AiClassifier.setConfig(config); - logger.aiLog("AiClassifier config updated from storage change", { debug: true }, config); - } if (changes.htmlToMarkdown) { htmlToMarkdown = changes.htmlToMarkdown.newValue === true; logger.aiLog("htmlToMarkdown updated from storage change", { debug: true }, htmlToMarkdown); @@ -517,13 +433,6 @@ async function clearCacheForMessages(idsInput) { collapseWhitespace = changes.collapseWhitespace.newValue === true; logger.aiLog("collapseWhitespace updated from storage change", { debug: true }, collapseWhitespace); } - if (changes.tokenReduction) { - tokenReduction = changes.tokenReduction.newValue === true; - logger.aiLog("tokenReduction updated from storage change", { debug: true }, tokenReduction); - } - if (changes.showDebugTab) { - showDebugTab = changes.showDebugTab.newValue === true; - } if (changes.errorPending) { errorPending = changes.errorPending.newValue === true; updateActionIcon(); diff --git a/manifest.json b/manifest.json index e7cb9d8..fe019ab 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,13 @@ { "manifest_version": 2, "name": "Sortana", - "version": "2.2.0", + "version": "2.1.1", "default_locale": "en-US", "applications": { "gecko": { "id": "ai-filter@jordanwages", "strict_min_version": "128.0", - "strict_max_version": "140.*" + "strict_max_version": "139.*" } }, "icons": { @@ -32,18 +32,17 @@ "page": "options/options.html", "open_in_tab": true }, - "permissions": [ - "storage", - "messagesRead", - "messagesMove", - "messagesUpdate", - "messagesTagsList", - "accountsRead", - "menus", - "notifications", - "scripting", - "tabs", - "theme", - "compose" - ] + "permissions": [ + "storage", + "messagesRead", + "messagesMove", + "messagesUpdate", + "messagesTagsList", + "accountsRead", + "menus", + "scripting", + "tabs", + "theme", + "compose" + ] } diff --git a/modules/AiClassifier.js b/modules/AiClassifier.js index 8313654..b35cb2c 100644 --- a/modules/AiClassifier.js +++ b/modules/AiClassifier.js @@ -308,11 +308,6 @@ async function classifyText(text, criterion, cacheKey = null) { } const payload = buildPayload(text, criterion); - try { - await storage.local.set({ lastPayload: JSON.parse(payload) }); - } catch (e) { - aiLog('failed to save last payload', { level: 'warn' }, e); - } aiLog(`[AiClassifier] Sending classification request to ${gEndpoint}`, {debug: true}); aiLog(`[AiClassifier] Classification request payload:`, { debug: true }, payload); diff --git a/options/options.html b/options/options.html index ddc5ee0..57f407c 100644 --- a/options/options.html +++ b/options/options.html @@ -31,10 +31,6 @@ .tag { --bulma-tag-h: 318; } - #diff-display { - white-space: pre-wrap; - font-family: monospace; - }
@@ -51,7 +47,6 @@