# Decisions — LexAI > Lightweight ADRs. One entry per material decision: what, why, what was rejected, when. Newest at top. Supersede rather than rewrite. ## Log ### D-2026-03-06-01 — BYO-LLM-key, no backend - **Status:** accepted - **Context:** Hosted writing assistants cost a subscription and route user text through a third party. Target users already hold LLM API keys. - **Decision:** No LexAI server. The background service worker calls the user's chosen provider (OpenAI/Anthropic/Groq/OpenRouter) directly with the user's key. - **Alternatives considered:** A thin proxy backend (rejected: adds cost, privacy surface, and an account system); on-device browser AI only (rejected: too limited across providers — noted as a Proofly reference pattern). - **Consequences:** Zero server cost and strong privacy story; shifts key handling and CORS entirely into the extension; no server-side rate limiting or abuse controls. - **Verification:** Manual — confirm no network calls leave the extension except to the selected provider endpoint. - **Owner / date:** Phase 1, 2026-03-06 ### D-2026-03-06-02 — Inline styles, no Tailwind - **Status:** accepted - **Context:** Content-script UI must not be broken by host-page CSS; WXT PostCSS/Tailwind integration was not wired. - **Decision:** Style all UI with inline style objects (`Object.assign(el.style, …)` / `style={{…}}`), using a dark Catppuccin-ish palette. - **Alternatives considered:** Tailwind (left in devDependencies but inactive); shadow DOM + stylesheet (deferred — see D-...-03). - **Consequences:** Reliable rendering on any host page; palette/button styles get duplicated across content/Options/Popup (refactor tracked in TASKS #5). - **Verification:** Visual check on multiple sites. - **Owner / date:** Phase 1, 2026-03-06 ### D-2026-03-06-03 — No shadow DOM; `data-lexai` guard instead - **Status:** accepted - **Context:** Injected toolbar/modal could collide with host-page styles or re-trigger LexAI's own handlers. - **Decision:** Inject directly into `document.body`; mark every LexAI node `data-lexai="true"` and skip events whose target is `closest('[data-lexai="true"]')`; use max `z-index` (2147483647). - **Alternatives considered:** Shadow DOM (rejected for now: added complexity; revisit if style isolation issues appear). - **Consequences:** Simple and working; weaker isolation than shadow DOM; highly customized editors (e.g. Google Docs) may not accept programmatic replace. - **Verification:** Manual across textarea/input/contenteditable sites. - **Owner / date:** Phase 1, 2026-03-06 ### D-2026-03-06-04 — Eager selection snapshot before async - **Status:** accepted - **Context:** Focus shifts to the toolbar and the live selection is gone by the time an async provider response returns. - **Decision:** Capture the active element + selection offsets eagerly (on `mouseup` and on button `mousedown`) and snapshot before any `await`; Replace uses the snapshot. Handle both textarea/input (`selectionStart/End`) and contenteditable/DOM (`Range` API). - **Alternatives considered:** Re-reading selection after the response (rejected: selection no longer exists). - **Consequences:** Replace works reliably; the pattern is fragile — editing content.ts must preserve snapshot-before-await. Zero automated coverage today (TASKS #10). - **Owner / date:** Phase 1, 2026-03-06 ### D-2026-03-06-05 — Dual `ANALYZE_TEXT` message shapes; `return true` listener - **Status:** accepted - **Context:** Content script/popup send `{ payload: {...} }`; other call sites send flat `{ text, action, style }`. Async responses need the message channel held open. - **Decision:** The handler accepts both shapes; the `onMessage` listener returns `true`. `fix` normalizes to `grammar`. - **Consequences:** Flexible but must be preserved in both forms; removing `return true` silently breaks all responses. - **Owner / date:** Phase 1, 2026-03-06 ### D-2026-xx-xx-06 — tweetnacl secretbox for the API key (obfuscation, not protection) - **Status:** accepted — flagged for revisit - **Context:** Storing the raw key in `chrome.storage.local` looked bad; added tweetnacl `secretbox` encryption (`apiKeyEnc` + `encKey`). - **Decision:** Prefer the encrypted path; keep plaintext `apiKey` as back-compat until a migration exists. - **Known weakness:** `encKey` is stored next to `apiKeyEnc`, so anyone who can read storage can decrypt. This is obfuscation, not protection (RECOMMENDATIONS #2). - **Alternatives to consider:** derive the key from `chrome.storage.session` / WebCrypto / a user passphrase; and be honest in the UI ("stored locally, obscured"). Tracked in TASKS #2. - **Owner / date:** post-Phase 1 ### D-2026-03-06-07 — Gitea CI + Chrome Web Store deploy - **Status:** accepted - **Context:** Project hosts CI on Gitea, not GitHub Actions. - **Decision:** `.gitea/workflows/ci.yml` (typecheck→test→build→zip to registry) and `deploy-chrome.yml` (on `v*.*.*` tag → CWS). Telegram notifications. Version must match in `package.json` and `wxt.config.ts`. - **Known weakness:** workflows `git clone` into `/tmp` and set `http.sslVerify false` (RECOMMENDATIONS #17). Revisit for speed/security. - **Owner / date:** Phase 1, 2026-03-06 ### D-2026-08-12-08 — CHANGELOG.md drives Gitea release notes - **Status:** accepted - **Context:** Releases previously shipped a hardcoded release body (`## LexAI ${VERSION}` + generic install steps) that never said what actually changed in that version. - **Decision:** Release notes live in `CHANGELOG.md` (Keep a Changelog format, Semantic Versioning). `.gitea/workflows/release.yml` extracts the section matching the pushed tag's version and uses it as the Gitea release body, with a generic fallback if no matching section exists. A version bump is not considered done until `CHANGELOG.md` has that version's section. - **Alternatives considered:** Auto-generating notes from commit messages (rejected: commit history is not curated for user-facing wording); keeping the hardcoded body (rejected: uninformative to installers). - **Consequences:** Every version bump now requires a `CHANGELOG.md` entry alongside the `package.json` bump; the release workflow degrades gracefully (generic body + logged warning) if that entry is missed rather than failing the release. - **Verification:** `release.yml` reviewed by an independent critic; P2 findings fixed. Confirmed locally that the section-extraction logic matches `## [1.1.0]` and stops at the next `## [` heading. - **Owner / date:** 2026-08-12 ## Open / proposed ### D-PROPOSED — Narrow host permissions from `` - **Status:** proposed (decide before serious Web Store push) - **Context:** Content script injects into every frame of every site, including banking/email/internal apps; also the #1 CWS review slowdown (RECOMMENDATIONS #1). - **Options:** `activeTab` + on-demand injection, or a user-configurable allowlist. - **Verification:** confirm actions still work after narrowing; measure review outcome.