# Architecture — LexAI > The current system and its important boundaries. Update in the same change that alters behavior; log the reason in `DECISIONS.md`. ## System at a glance - **Shape:** Browser extension (Chrome, Manifest V3) with three cooperating contexts message-passed over `chrome.runtime`. No backend. - **Stack:** WXT `^0.20` (wraps Vite), React 18 + TypeScript (Options/Popup pages only), tweetnacl/tweetnacl-util for key encryption, Zustand installed but unused. - **Data stores:** `chrome.storage.local` (provider, model, `apiKeyEnc`, `encKey`, legacy plaintext `apiKey`). No server, no DB. - **Hosting / deploy:** Chrome Web Store. Build output `.output/chrome-mv3/`. - **Build / release:** Gitea CI (`.gitea/workflows/`) → typecheck → test → build → zip to Gitea registry; `v*.*.*` tag → Chrome Web Store deploy. ## Component map ```text entrypoints/content.ts (content script, injected into ) • Detects selection: textarea/input (selectionStart/End) vs contenteditable/DOM (Range API) • Renders floating toolbar + result modal + toasts (inline-styled, appended to document.body) • Snapshots selection state BEFORE any async call; Replace uses the snapshot • Sends { type: 'ANALYZE_TEXT', payload: {text, action, style} } to background entrypoints/background.ts (service worker — the LLM proxy) • onMessage: ANALYZE_TEXT and COPY_AS (returns true to keep async channel open) • Reads provider/apiKey/apiKeyEnc/encKey/model from chrome.storage.local • Decrypts key (tweetnacl secretbox), routes to the correct provider fetch • Registers right-click context menus (action × style) on install entrypoints/options/Options.tsx (React settings page) • Provider + model + API key form; encrypts key → apiKeyEnc/encKey in storage entrypoints/popup/Popup.tsx (React toolbar popup) • Standalone text box → same ANALYZE_TEXT flow; shows config status; links to Options ``` | Component | Responsibility | Owns (paths) | Talks to | Notes | | --- | --- | --- | --- | --- | | content script | selection, toolbar/modal UI, replace | `entrypoints/content.ts` | background via messages | DOM-timing-sensitive; snapshot before await | | background SW | LLM proxy, key decrypt, routing, context menus | `entrypoints/background.ts` | provider APIs, storage | only context allowed to fetch providers | | options page | provider/model/key config + encrypt | `entrypoints/options/Options.tsx` | storage | React | | popup | standalone analyze + status | `entrypoints/popup/Popup.tsx` | background via messages | React | | shared utils | reference notes, future shared code | `src/utils/**` | — | see REFERENCE_NOTES.md | ## Boundaries and contracts - **CORS/key boundary:** Content script and popup **must not** call provider APIs. All provider `fetch` and key handling live in `background.ts`. Route through `ANALYZE_TEXT` / `COPY_AS`. - **Message contract:** - `ANALYZE_TEXT` accepts **both** `{ payload: {text, action, style} }` and flat `{ text, action, style }` — keep both if you touch the handler. - `action` ∈ `grammar | rephrase | shorten | expand | explain`. Context menu/popup emit `fix`, normalized to `grammar` by `getSystemPrompt`. - The `onMessage` listener **must `return true`** to keep the async channel open; removing it silently breaks every response. - **Provider layer:** Each provider is duplicated — `callX` (system prompt from action) and `callXWithPrompt` (arbitrary prompt, used by COPY_AS). Request-shape changes usually need both. (Known smell — see DECISIONS + TASKS.) - **DOM guard:** Every injected node carries `data-lexai="true"`; handlers check `closest('[data-lexai="true"]')` to avoid self-triggering. `z-index: 2147483647` keeps UI above host pages. ## Data model (essentials) - **Storage keys:** `provider`, `model`, `apiKeyEnc`, `encKey`, `apiKey` (legacy plaintext, back-compat only). - **Sensitive data:** the LLM API key. Prefer the encrypted path; never log it; never transmit except to the user's selected provider. Don't drop the plaintext fallback without a migration. ## Cross-cutting concerns - **Config/secrets:** provider list + default models + endpoints currently duplicated across `Options.tsx` and `background.ts` (drift risk — see TASKS). - **Observability:** intentional `[LexAI …]` console logs in content.ts replace path (should be gated behind a DEV flag — see TASKS). - **Testing:** Vitest (jsdom) unit + Playwright e2e. Unit tests currently exercise the `chrome.storage` mock rather than importing real handlers; e2e has `[EXTENSION_ID]` placeholders and won't pass as-is (see TASKS/EVALS). ## Known constraints and debt - Tailwind inactive; all UI is inline style objects (WXT PostCSS never wired). Do not assume Tailwind classes work. - No shadow DOM; UI injected directly into `document.body`, isolated only by `data-lexai` + max z-index. - Version bump is a manual two-file edit (`package.json` + `wxt.config.ts`). --- *Record non-obvious choices in `DECISIONS.md`; keep exposure current in `attacksurface.md`.*