|
1 | | -AGENTS.md |
| 1 | +# Widget |
| 2 | + |
| 3 | +Embeddable support widget customers drop into their own product via a script tag. Published to npm as `@marketrix.ai/widget`. Runs inside arbitrary host pages, so host-page safety, predictable runtime, and bundle size are first-class concerns. |
| 4 | + |
| 5 | +Stack: React 19 (peer dep), Vite 8, TypeScript 6, Tailwind CSS 4 (`@tailwindcss/vite`), Zod 4, oRPC 1, `@rrweb/record` for session recording, `@base-ui/react` for primitives. Output is a single ESM bundle `dist/widget.mjs` plus a classic `loader.js`. |
| 6 | + |
| 7 | +Dev server: `npm start` on port **9001** (`http://widget.marketrix.localhost`). Port is overridable via `PORT` / `VITE_PORT`. |
| 8 | + |
| 9 | +## Commands |
| 10 | + |
| 11 | +```bash |
| 12 | +npm start # vite dev server on :9001 |
| 13 | +npm run build # vite build → dist/widget.mjs (terser) + tsc declarations |
| 14 | +npm run code:check # tsc --noEmit && eslint && prettier --check (one-shot gate) |
| 15 | +npm run code:fix # eslint --fix && prettier --write |
| 16 | +npm run type-check # tsc --noEmit (alias: check) |
| 17 | +npm run lint # eslint --fix (max-warnings 200); lint:check = max-warnings 0 |
| 18 | +npm run format:check # prettier --check |
| 19 | +npm run test:run # vitest run (jsdom + Testing Library + axe) |
| 20 | +npm run test # vitest watch (test:ui = watch, test:coverage = v8) |
| 21 | +npm run visual:check # scripts/visual-check.mjs — gates on rendered UI |
| 22 | +npm run a11y:check # scripts/a11y-check.mjs — gates on accessibility |
| 23 | +npm run bundle:check # scripts/bundle-check.mjs — gates on bundle size |
| 24 | +npm run tag <version> # scripts/release.sh — see Release below |
| 25 | +``` |
| 26 | + |
| 27 | +**Pre-handoff gates** (mirrors `ci.yml` `validate` job): `type-check`, `lint`, `build`, `format:check`, `test:run`, then `visual:check` + `a11y:check` + `bundle:check`. Git hooks (lefthook) run type-check + autofixing lint, but they are not a substitute — run the full set. |
| 28 | + |
| 29 | +## Runtime model |
| 30 | + |
| 31 | +- Mounts into a **closed Shadow DOM** (`attachShadow({ mode: 'closed' })` in `src/utils/bootstrap.tsx`) to prevent host CSS/DOM leakage. CSS is injected into the shadow root via `vite-plugin-css-injected-by-js` (no external stylesheet). |
| 32 | +- `window.__mtx` is the **singleton guard** — duplicate/concurrent `initWidget` calls are deduplicated. |
| 33 | +- Three credential modes (auto-detected by `mountWidget`): |
| 34 | + - **Production**: `mtxId` + `mtxKey` |
| 35 | + - **Dev**: `mtxApp` (application id) + `mtxAgent` (agent id) |
| 36 | + - **Preview**: rendered via the `<MarketrixWidget>` React component with inline `settings` |
| 37 | +- All modes require `mtxApiHost`. |
| 38 | + |
| 39 | +## Transport (widget ↔ api) |
| 40 | + |
| 41 | +Two typed oRPC procedures, both defined in `src/sdk/contracts/widget.ts`: |
| 42 | + |
| 43 | +- **`widgetStream`** — GET `/widget/stream`, an SSE `eventIterator(WidgetEventSchema)`. Server → widget events. |
| 44 | +- **`widgetMessage`** — POST `/widget/message`, body `{ chat_id, command: WidgetCommandSchema }`. Widget → server commands. |
| 45 | + |
| 46 | +Both schemas are **discriminated unions on `type`**: |
| 47 | + |
| 48 | +- `WidgetEvent` types: `registered`, `pong`, `heartbeat`, `chat/response`, `chat/error`, `task/status`, `tool/call`. |
| 49 | +- `WidgetCommand` types: `chat/tell`, `chat/show`, `chat/do`, `chat/stop`, `tool/response`, `ping`, `rrweb/metadata`, `rrweb/events`. |
| 50 | + |
| 51 | +**Task status vocabulary (Wave 14):** `task/status.status` is `z.enum(['running', 'completed', 'failed', 'stopped', 'has_question'])`. Legacy `'started'` / `'in_progress'` were dropped — the widget now emits/consumes `'running'`. Matches the agent-side `SimulationTaskStatus` / `QATaskStatus`. |
| 52 | + |
| 53 | +Interaction modes map to commands: **Tell** = `chat/tell` (explain), **Show** = `chat/show` (`tool/call` with `mode: 'show'`, visual highlighting), **Do** = `chat/do` (`tool/call` with `mode: 'do'`, performs browser actions). |
| 54 | + |
| 55 | +## SDK mirror (must stay in sync) |
| 56 | + |
| 57 | +The widget's SDK is a **scoped mirror** of the API contract, generated from the source of truth in the `api` repo: |
| 58 | + |
| 59 | +| Source of truth (`api` repo) | Widget mirror | |
| 60 | +|---|---| |
| 61 | +| `api/contracts/widget.ts` + `contracts/*.ts` | `widget/src/sdk/contracts/*.ts` | |
| 62 | +| `api/sdk/widget.ts` (scoped aggregate) | `widget/src/sdk/contract.ts` | |
| 63 | + |
| 64 | +The widget mirror is per-domain contract fragments under `src/sdk/contracts/` (`widget.ts`, `agent.ts`, `application.ts`, `chat.ts`, `entities.ts`, `common.ts`, `activityLog.ts`) — there is **no** `src/sdk/routes.ts` or `src/sdk/schema.ts` (those are stale references). `src/sdk/contract.ts` assembles the `widgetContract` object; `src/sdk/index.ts` exports the oRPC client (`sdk`) plus `configureSdk()` and re-exports `WidgetEventSchema`, `WidgetSettingsDataSchema`, and contract types. |
| 65 | + |
| 66 | +Drift is enforced by `.github/workflows/contract-drift.yml`: it sparse-checkouts `api@dev` and runs api's `scripts/sync-consumers.mjs widget --check`. **It requires the `CONTRACTS_READ_TOKEN` repo/org secret** (fine-grained PAT, Contents:read on `Marketrix-ai/api`) or it fails fast. Don't hand-edit `src/sdk/contracts/*` — regenerate from the api side and run the sync. |
| 67 | + |
| 68 | +## Key services (`src/services/`) |
| 69 | + |
| 70 | +- `StreamClient.ts` — SSE lifecycle: registration, auth-failure handling, exponential-backoff reconnects. |
| 71 | +- `SessionRecorder.ts` — rrweb batching + POST flush. Thresholds: flush every **500 ms** (`FLUSH_INTERVAL_MS`) or at **50 KB** queued (`FLUSH_SIZE_THRESHOLD`); batches capped at **500 KB** (`MAX_BATCH_BYTES`, stays under the API body-parser limit); queue capped at 500 events; drops after 5 consecutive flush failures. Don't change these without updating the API's expectations. |
| 72 | +- `ToolService.ts` / `ShowModeService.ts` / `DomService.ts` — tool execution, Show-mode highlighting, host-page DOM interaction. |
| 73 | +- `ChatService.ts`, `SessionManager.ts`, `StorageService.ts`, `ConfigManager.ts`, `ValidationService.ts`, `WidgetService.ts`, `ApiService.ts`, `ScreenShareService.ts`. |
| 74 | + |
| 75 | +## Structure |
| 76 | + |
| 77 | +- `src/components/` — UI (`base/`, `blocks/`, `chat/`, `navigation/`, `ui/`, `views/`). |
| 78 | +- `src/design-system/` — design tokens + primitives (keep token usage consistent). |
| 79 | +- `src/services/` — see above. `src/hooks/`, `src/context/`, `src/utils/`, `src/lib/` — client state + helpers. |
| 80 | +- `src/sdk/` — oRPC client + contract mirror (see above). |
| 81 | +- `src/index.tsx` — public entry: `initWidget`, `mountWidget`, `unmountWidget`, `updateMarketrixConfig`, `getCurrentConfig`, `startRecording`/`stopRecording`/`getRecordingState`, `MarketrixWidget`. |
| 82 | +- `src/test/` + `src/**/__tests__/` — vitest setup, fixtures, a11y helpers. Name tests `*.test.ts(x)` near the feature. |
| 83 | +- `public/loader.js` — classic script-tag loader. `dist/` — generated only. |
| 84 | + |
| 85 | +## Release |
| 86 | + |
| 87 | +`@marketrix.ai/widget` is npm-published. Version is currently `3.8.9`. |
| 88 | + |
| 89 | +1. `npm run tag <version>` (e.g. `npm run tag 3.8.10`) runs `scripts/release.sh`: bumps `package.json`, **`npm install` to refresh `package-lock.json`**, `npm run build`, commits `package.json` + `package-lock.json` as `chore(widget): release vX`, and creates annotated tag `vX`. |
| 90 | +2. Push: `git push origin HEAD && git push origin v<version>`. |
| 91 | +3. The `v*` tag triggers `ci.yml`: `build` job → `marketrix.azurecr.io/widget:<version>` (v-prefix stripped for image tag), `publish` job → npm (skipped if that version already exists, so sync-only tags are safe). |
| 92 | +4. Bumping the app's pinned version: `cd ../app && npm install @marketrix.ai/widget@latest`, then deploy widget + app together via the centralized `deploy.yml` in `infra`. |
| 93 | + |
| 94 | +**Lockfile rule:** any version bump or dependency change must run `npm install` and commit `package-lock.json` alongside `package.json`. `npm run tag` does this for you. |
| 95 | + |
| 96 | +## Conventions |
| 97 | + |
| 98 | +- TS, 2-space indent, single quotes, semicolons, trailing commas, ~120-char lines. `type` imports, sorted imports (simple-import-sort). `PascalCase` components/services/context, `useCamelCase` hooks, `camelCase` utils. |
| 99 | +- Don't develop on `dev` — use a worktree/feature branch. Link the PR with `Closes #N`. Issue/PR body is the scope source of truth. |
| 100 | +- Prefer extending shared services/utils over one-off logic in a component. |
0 commit comments