From 60c4d4fec06e2a113566ee1b3de228c17543c6a4 Mon Sep 17 00:00:00 2001 From: Parth576 Date: Sat, 28 Feb 2026 22:11:13 -0500 Subject: [PATCH 1/3] feat(extension): scaffold extension directory with manifest and browser compat Create the browser extension foundation with Manifest V3 configuration, browser API compatibility polyfill, placeholder icons, and stub files for background worker, content script, and popup UI. Assisted by the code-assist SOP --- .../context.md | 27 ++++++ .../extension-scaffold-and-manifest/plan.md | 40 +++++++++ .../progress.md | 31 +++++++ extension/background/worker.js | 10 +++ extension/content/extract.js | 10 +++ extension/icons/icon-128.png | Bin 0 -> 306 bytes extension/icons/icon-16.png | Bin 0 -> 79 bytes extension/icons/icon-48.png | Bin 0 -> 124 bytes extension/manifest.json | 37 ++++++++ extension/popup/popup.css | 25 ++++++ extension/popup/popup.html | 24 +++++ extension/popup/popup.js | 11 +++ extension/utils/browser-compat.js | 82 ++++++++++++++++++ 13 files changed, 297 insertions(+) create mode 100644 .agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/context.md create mode 100644 .agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/plan.md create mode 100644 .agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/progress.md create mode 100644 extension/background/worker.js create mode 100644 extension/content/extract.js create mode 100644 extension/icons/icon-128.png create mode 100644 extension/icons/icon-16.png create mode 100644 extension/icons/icon-48.png create mode 100644 extension/manifest.json create mode 100644 extension/popup/popup.css create mode 100644 extension/popup/popup.html create mode 100644 extension/popup/popup.js create mode 100644 extension/utils/browser-compat.js diff --git a/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/context.md b/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/context.md new file mode 100644 index 0000000..2cd53bf --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/context.md @@ -0,0 +1,27 @@ +# Context: Extension Scaffold and Manifest + +## Requirements + +1. Create full `extension/` directory structure with all required files +2. Create valid Manifest V3 `manifest.json` (Firefox + Chrome compatible) +3. Create placeholder icon PNGs at 16x16, 48x48, 128x128 +4. Create `utils/browser-compat.js` normalizing `browser.*`/`chrome.*` APIs +5. Create placeholder files for background worker, content script, and popup + +## Key Design Decisions + +- **No build system**: Vanilla JS loaded directly +- **Manifest V3**: Current standard for both Firefox and Chrome +- **Minimal permissions**: `activeTab` and `storage` only +- **Browser compat polyfill**: Wraps Promise-based (`browser.*`) vs callback-based (`chrome.*`) differences +- **APIs to normalize**: `runtime.sendMessage`, `runtime.onMessage`, `storage.local.get/set`, `tabs.query`, `scripting.executeScript` + +## Existing Patterns + +- Project uses Go backend with existing `backend/` directory +- Extension is a separate top-level `extension/` directory +- No CODEASSIST.md found + +## Implementation Path + +All files go under `extension/` at the repo root. No dependencies on backend code. diff --git a/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/plan.md b/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/plan.md new file mode 100644 index 0000000..3d2e606 --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/plan.md @@ -0,0 +1,40 @@ +# Plan: Extension Scaffold and Manifest + +## Testing Strategy + +Per the design doc, extension testing is manual only for MVP: +- Load unpacked in Firefox and Chrome +- Verify no console errors +- Verify manifest loads without warnings + +No automated tests for this task. + +## Implementation Plan + +### 1. Create directory structure +Create `extension/` with subdirectories: `background/`, `content/`, `popup/`, `icons/`, `utils/` + +### 2. Create manifest.json +Manifest V3 with: +- `manifest_version: 3` +- Permissions: `activeTab`, `storage` +- Background service worker +- Content scripts (all URLs, document_idle) +- Browser action with popup and icons +- `browser_specific_settings` for Firefox compatibility (gecko ID) + +### 3. Generate placeholder icons +Use Python/ImageMagick to create simple solid-color PNGs at 16x16, 48x48, 128x128 + +### 4. Create browser-compat.js +Normalize `browser.*` vs `chrome.*`: +- Detect available API namespace +- For Chrome: wrap callback-based APIs in Promises +- For Firefox: use native `browser.*` directly +- Export unified API object + +### 5. Create placeholder files +Minimal boilerplate with comments for: +- `background/worker.js` +- `content/extract.js` +- `popup/popup.html`, `popup/popup.js`, `popup/popup.css` diff --git a/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/progress.md b/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/progress.md new file mode 100644 index 0000000..0aca778 --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/extension-scaffold-and-manifest/progress.md @@ -0,0 +1,31 @@ +# Progress: Extension Scaffold and Manifest + +## Setup +- [x] Created documentation directory structure +- [x] Discovered instruction files (README.md in backend/) +- [x] Read detailed design document +- [x] Created context.md + +## Implementation Checklist +- [x] Create directory structure (`extension/` with subdirectories) +- [x] Create `manifest.json` (Manifest V3, Firefox + Chrome) +- [x] Create placeholder icon PNGs (16x16, 48x48, 128x128) +- [x] Create `utils/browser-compat.js` (API normalization polyfill) +- [x] Create `background/worker.js` (placeholder) +- [x] Create `content/extract.js` (placeholder) +- [x] Create `popup/popup.html` (placeholder) +- [x] Create `popup/popup.js` (placeholder) +- [x] Create `popup/popup.css` (placeholder) +- [x] Verify manifest is valid JSON +- [x] Verify icons are valid PNGs at correct sizes + +## Validation +- Manifest validates as JSON with correct manifest_version (3), permissions (activeTab, storage), service worker, and content scripts +- Icons are valid PNGs at 16x16, 48x48, and 128x128 +- All 10 files present in correct directory structure +- No automated tests (extension uses manual testing per design doc) + +## Notes +- Used teal/green (#2d9c6f) for placeholder icon color +- Firefox compat via `browser_specific_settings.gecko` with strict_min_version 109.0 (first Firefox version with full MV3 support) +- Browser compat polyfill wraps: runtime.sendMessage, runtime.onMessage, storage.local.get/set, tabs.query, scripting.executeScript diff --git a/extension/background/worker.js b/extension/background/worker.js new file mode 100644 index 0000000..00df81e --- /dev/null +++ b/extension/background/worker.js @@ -0,0 +1,10 @@ +/** + * Background service worker for SmolTerms. + * Handles communication between content script, popup, and the backend API. + * + * TODO: Implement in task-02 (background worker) + * - Listen for messages from popup to trigger analysis + * - Send extracted HTML to backend API + * - Forward analysis results back to popup + * - Handle errors and loading states + */ diff --git a/extension/content/extract.js b/extension/content/extract.js new file mode 100644 index 0000000..7e09a6e --- /dev/null +++ b/extension/content/extract.js @@ -0,0 +1,10 @@ +/** + * Content script for SmolTerms. + * Extracts main content HTML from the current page. + * + * TODO: Implement in task-03 (content extraction) + * - Extract document.body.innerHTML or main content area + * - Strip nav, footer, sidebar elements + * - Listen for extraction requests from background worker + * - Send extracted HTML back to background worker + */ diff --git a/extension/icons/icon-128.png b/extension/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..8ea248788dd3b7d52f8a6c76abdbccaf6ac25488 GIT binary patch literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRdwrKRsO>Ln`LHz39lxpultBfTWXj zn?QGuZIvgh$xh4nSsNqmJJ=ZV`wO2VA#UUut1i9;VlD0#?Y0rv%SGG VxiEUO1~3p9JYD@<);T3K0RYLyN^AfC literal 0 HcmV?d00001 diff --git a/extension/icons/icon-16.png b/extension/icons/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..39e166e05561d13e3b4b3cc5fe99db9f49a6b116 GIT binary patch literal 79 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|VxBIJAr*6yFU+0nFO(#4)nHZP ck1Pg;xy%*"], + "js": ["content/extract.js"], + "run_at": "document_idle" + } + ], + "browser_specific_settings": { + "gecko": { + "id": "smolterms@example.com", + "strict_min_version": "109.0" + } + } +} diff --git a/extension/popup/popup.css b/extension/popup/popup.css new file mode 100644 index 0000000..dbaca30 --- /dev/null +++ b/extension/popup/popup.css @@ -0,0 +1,25 @@ +/** + * Popup styles for SmolTerms. + * + * TODO: Implement in task-04 (popup UI) + * - Layout for score display + * - Risk level color coding (green, yellow, orange, red) + * - Typography and spacing + * - Loading spinner + * - Error state styling + */ + +body { + width: 360px; + min-height: 200px; + margin: 0; + padding: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + color: #1a1a1a; +} + +h1 { + margin: 0 0 8px; + font-size: 18px; +} diff --git a/extension/popup/popup.html b/extension/popup/popup.html new file mode 100644 index 0000000..0bf4187 --- /dev/null +++ b/extension/popup/popup.html @@ -0,0 +1,24 @@ + + + + + + SmolTerms + + + +
+ +

SmolTerms

+

Privacy policy analyzer

+
+ + + + diff --git a/extension/popup/popup.js b/extension/popup/popup.js new file mode 100644 index 0000000..f8dc14d --- /dev/null +++ b/extension/popup/popup.js @@ -0,0 +1,11 @@ +/** + * Popup script for SmolTerms. + * Renders analysis results in the extension popup. + * + * TODO: Implement in task-04 (popup UI) + * - Request analysis from background worker on popup open + * - Render overall score with risk level color + * - Display per-dimension score breakdown + * - Show key concerns and summary + * - Handle loading and error states + */ diff --git a/extension/utils/browser-compat.js b/extension/utils/browser-compat.js new file mode 100644 index 0000000..60ce5a0 --- /dev/null +++ b/extension/utils/browser-compat.js @@ -0,0 +1,82 @@ +/** + * Browser API compatibility layer. + * Normalizes browser.* (Firefox) and chrome.* (Chrome) APIs + * into a unified Promise-based interface. + */ + +const browserAPI = (() => { + // Firefox provides the Promise-based browser.* namespace natively + if (typeof browser !== "undefined" && browser.runtime) { + return browser; + } + + // Chrome uses callback-based chrome.* APIs — wrap them in Promises + if (typeof chrome !== "undefined" && chrome.runtime) { + return { + runtime: { + sendMessage: (...args) => + new Promise((resolve, reject) => { + chrome.runtime.sendMessage(...args, (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(response); + } + }); + }), + onMessage: chrome.runtime.onMessage, + getURL: chrome.runtime.getURL.bind(chrome.runtime), + }, + storage: { + local: { + get: (keys) => + new Promise((resolve, reject) => { + chrome.storage.local.get(keys, (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(result); + } + }); + }), + set: (items) => + new Promise((resolve, reject) => { + chrome.storage.local.set(items, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + }), + }, + }, + tabs: { + query: (queryInfo) => + new Promise((resolve, reject) => { + chrome.tabs.query(queryInfo, (tabs) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(tabs); + } + }); + }), + }, + scripting: { + executeScript: (injection) => + new Promise((resolve, reject) => { + chrome.scripting.executeScript(injection, (results) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(results); + } + }); + }), + }, + }; + } + + throw new Error("No compatible browser API found"); +})(); From c3a770cbb6ea790e8d7f73796c03d3e37206fea1 Mon Sep 17 00:00:00 2001 From: Parth576 Date: Sat, 28 Feb 2026 22:38:33 -0500 Subject: [PATCH 2/3] feat(extension): implement content script and background service worker Add content extraction logic that intelligently selects the main content area (main > article > [role=main] > body), clones the DOM to avoid mutation, and strips non-content elements (scripts, styles, nav, footer, header, sidebars). Add background worker that orchestrates the analysis flow: receives requests from the popup, triggers content extraction via messaging, POSTs to the backend API with timeout/error handling, and returns results to the popup. Also adds tabs.sendMessage to the browser compat layer and includes browser-compat.js in manifest content_scripts for the content script. Assisted by the code-assist SOP --- .../context.md | 39 ++++ .../content-script-background-worker/plan.md | 96 ++++++++++ .../progress.md | 34 ++++ extension/background/worker.js | 174 +++++++++++++++++- extension/content/extract.js | 137 +++++++++++++- extension/manifest.json | 2 +- extension/utils/browser-compat.js | 10 + 7 files changed, 481 insertions(+), 11 deletions(-) create mode 100644 .agents/scratchpad/2026-02-15-smolterms/content-script-background-worker/context.md create mode 100644 .agents/scratchpad/2026-02-15-smolterms/content-script-background-worker/plan.md create mode 100644 .agents/scratchpad/2026-02-15-smolterms/content-script-background-worker/progress.md diff --git a/.agents/scratchpad/2026-02-15-smolterms/content-script-background-worker/context.md b/.agents/scratchpad/2026-02-15-smolterms/content-script-background-worker/context.md new file mode 100644 index 0000000..480feb4 --- /dev/null +++ b/.agents/scratchpad/2026-02-15-smolterms/content-script-background-worker/context.md @@ -0,0 +1,39 @@ +# Context: Content Script & Background Worker + +## Project Structure +- Extension: Vanilla JS, Manifest V3, no build system +- Files to implement: `extension/content/extract.js`, `extension/background/worker.js` +- Dependency: `extension/utils/browser-compat.js` (already implemented - provides `browserAPI` global) + +## Key Patterns +- `browserAPI` is a global IIFE that normalizes Firefox `browser.*` and Chrome `chrome.*` +- Available APIs: `runtime.sendMessage`, `runtime.onMessage`, `storage.local.get/set`, `tabs.query`, `scripting.executeScript` +- Manifest V3 service worker pattern (no persistent background page) +- Content script registered in manifest with `"run_at": "document_idle"` +- `browser-compat.js` is NOT imported via ES modules - it's a script tag in popup.html but for content/background it would need `importScripts` or manifest injection + +## API Contract +- POST /api/v1/analyze: `{ url: string, html: string }` -> `AnalysisResult` +- AnalysisResult: `{ url, overall_score, risk_level, dimensions, key_concerns, summary, cached, analyzed_at }` +- Error: `{ error: string }` with HTTP 4xx/5xx +- Timeout: 60s (504 from backend) + +## Messaging Protocol (from task spec) +- Popup -> Background: `{ type: "analyze" }` +- Background -> Content: `{ type: "extract" }` +- Content -> Background: `{ type: "extracted", html: "...", url: "..." }` +- Background -> Popup: `{ type: "result", data: {...} }` or `{ type: "error", message: "..." }` + +## Content Extraction Rules +- Strip: ` diff --git a/extension/popup/popup.js b/extension/popup/popup.js index f8dc14d..47ad01b 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -2,10 +2,280 @@ * Popup script for SmolTerms. * Renders analysis results in the extension popup. * - * TODO: Implement in task-04 (popup UI) - * - Request analysis from background worker on popup open - * - Render overall score with risk level color - * - Display per-dimension score breakdown - * - Show key concerns and summary - * - Handle loading and error states + * Sends an "analyze" message to the background worker on open, + * then renders the appropriate UI state based on the response. */ + +/* ─── Constants ─── */ + +/** Map of dimension keys to human-readable labels. */ +const DIMENSION_LABELS = { + data_collection: "Data Collection", + data_sharing: "Data Sharing", + user_rights: "User Rights", + retention: "Retention", + security: "Security", +}; + +/** Display order for dimensions. */ +const DIMENSION_ORDER = [ + "data_collection", + "data_sharing", + "user_rights", + "retention", + "security", +]; + +/* ─── Risk Level Helpers ─── */ + +/** + * Returns the risk level for a given score. + * + * @param {number} score - Score from 1-10. + * @returns {{ level: string, label: string, className: string }} + */ +function getRiskLevel(score) { + if (score >= 8) { + return { level: "low", label: "Low Risk", className: "risk-low" }; + } + if (score >= 5) { + return { level: "moderate", label: "Moderate Risk", className: "risk-moderate" }; + } + if (score >= 3) { + return { level: "high", label: "High Risk", className: "risk-high" }; + } + return { level: "critical", label: "Critical Risk", className: "risk-critical" }; +} + +/** + * Returns the bar CSS class for a given score. + * + * @param {number} score - Score from 1-10. + * @returns {string} CSS class name for the bar color. + */ +function getBarClass(score) { + if (score >= 8) return "bar-low"; + if (score >= 5) return "bar-moderate"; + if (score >= 3) return "bar-high"; + return "bar-critical"; +} + +/* ─── DOM Helpers ─── */ + +/** Cached section elements. */ +const sections = { + loading: document.getElementById("state-loading"), + results: document.getElementById("state-results"), + error: document.getElementById("state-error"), + notPolicy: document.getElementById("state-not-policy"), +}; + +/** + * Shows a single UI state and hides all others. + * + * @param {"loading"|"results"|"error"|"notPolicy"} stateName + */ +function showState(stateName) { + for (const [key, el] of Object.entries(sections)) { + if (key === stateName) { + el.classList.remove("hidden"); + } else { + el.classList.add("hidden"); + } + } +} + +/* ─── Render Functions ─── */ + +/** Shows the loading state. */ +function renderLoading() { + showState("loading"); +} + +/** + * Renders the analysis results. + * + * @param {object} data - The analysis result from the backend API. + */ +function renderResults(data) { + const risk = getRiskLevel(data.overall_score); + + // Overall score + const scoreCircle = document.getElementById("score-circle"); + const scoreValue = document.getElementById("score-value"); + const riskBadge = document.getElementById("risk-badge"); + + scoreValue.textContent = data.overall_score.toFixed(1); + + // Apply risk color to circle + scoreCircle.className = "score-circle " + risk.className; + + // Risk badge + riskBadge.textContent = risk.label; + riskBadge.className = "risk-badge " + risk.className; + + // Cached indicator + const cachedBadge = document.getElementById("cached-badge"); + if (data.cached) { + cachedBadge.classList.remove("hidden"); + } else { + cachedBadge.classList.add("hidden"); + } + + // Dimension scores + renderDimensions(data.dimensions); + + // Key concerns + renderConcerns(data.key_concerns); + + // Summary + const summarySection = document.getElementById("summary-section"); + const summaryText = document.getElementById("summary-text"); + if (data.summary) { + summaryText.textContent = data.summary; + summarySection.classList.remove("hidden"); + } else { + summarySection.classList.add("hidden"); + } + + showState("results"); +} + +/** + * Renders the five dimension scores as rows with progress bars. + * + * @param {object} dimensions - Map of dimension key to { score, summary }. + */ +function renderDimensions(dimensions) { + const container = document.getElementById("dimensions"); + container.innerHTML = ""; + + for (const key of DIMENSION_ORDER) { + const dim = dimensions[key]; + if (!dim) continue; + + const label = DIMENSION_LABELS[key] || key; + const barClass = getBarClass(dim.score); + const widthPercent = (dim.score / 10) * 100; + + const row = document.createElement("div"); + row.className = "dimension-row"; + row.title = dim.summary || ""; + + row.innerHTML = + '' + escapeHtml(label) + "" + + '
' + + '
' + + "
" + + '' + dim.score.toFixed(1) + ""; + + container.appendChild(row); + } +} + +/** + * Renders the key concerns list. + * + * @param {string[]} concerns - Array of concern strings. + */ +function renderConcerns(concerns) { + const section = document.getElementById("concerns-section"); + const list = document.getElementById("concerns-list"); + list.innerHTML = ""; + + if (!concerns || concerns.length === 0) { + section.classList.add("hidden"); + return; + } + + section.classList.remove("hidden"); + + for (const concern of concerns) { + const li = document.createElement("li"); + li.className = "concern-item"; + li.textContent = concern; + list.appendChild(li); + } +} + +/** + * Renders the error state with a message and retry button. + * + * @param {string} message - The error message to display. + */ +function renderError(message) { + const errorMessage = document.getElementById("error-message"); + errorMessage.textContent = message || "An unexpected error occurred."; + showState("error"); +} + +/** Renders the "not a privacy policy" state. */ +function renderNotPolicy() { + showState("notPolicy"); +} + +/* ─── Utilities ─── */ + +/** + * Escapes HTML special characters to prevent XSS. + * + * @param {string} str - Raw string. + * @returns {string} Escaped string safe for innerHTML. + */ +function escapeHtml(str) { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +/* ─── Analysis Flow ─── */ + +/** + * Triggers analysis by sending a message to the background worker + * and handles the response. + */ +async function triggerAnalysis() { + renderLoading(); + + try { + const response = await browserAPI.runtime.sendMessage({ type: "analyze" }); + + if (!response) { + renderError("No response from background worker. Try reloading the extension."); + return; + } + + if (response.type === "error") { + renderError(response.message); + return; + } + + if (response.type === "result" && response.data) { + // Check for "not a policy" response + if (response.data.risk_level === "not_policy") { + renderNotPolicy(); + return; + } + + renderResults(response.data); + return; + } + + // Unexpected response shape + renderError("Received an unexpected response. Please try again."); + } catch (err) { + renderError(err.message || "Failed to communicate with the extension."); + } +} + +/* ─── Event Listeners ─── */ + +// Retry button +document.getElementById("retry-button").addEventListener("click", () => { + triggerAnalysis(); +}); + +/* ─── Init ─── */ + +// Start analysis immediately when popup opens +triggerAnalysis();