Some checks failed
CI — Test & Build / Test & Build (push) Failing after 39s
- Added a new "Prompt Builder" tab in the Popup for generating AI prompts with customizable parameters. - Introduced new state variables for managing prompt styles, personas, formats, and models. - Enhanced the Options page to fetch and display models based on the provided API key. - Updated the actions and types to include the new 'prompt' action and its associated parameters. - Implemented migration logic for legacy plaintext API keys to encrypted storage. - Updated the getSystemPrompt function to incorporate prompt parameters for better instruction generation. - Added tests for the new functionality, including context menu entries and prompt generation logic.
5.0 KiB
5.0 KiB
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 plaintextapiKey). 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
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
fetchand key handling live inbackground.ts. Route throughANALYZE_TEXT/COPY_AS. - Message contract:
ANALYZE_TEXTaccepts 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 emitfix, normalized togrammarbygetSystemPrompt.- The
onMessagelistener mustreturn trueto keep the async channel open; removing it silently breaks every response.
- Provider layer: Each provider is duplicated —
callX(system prompt from action) andcallXWithPrompt(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 checkclosest('[data-lexai="true"]')to avoid self-triggering.z-index: 2147483647keeps 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.tsxandbackground.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.storagemock 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 bydata-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.