# 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 <all_urls>)
  • 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`.*
