chore: import upstream snapshot with attribution
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:48:47 +08:00
commit b7f52be4c9
653 changed files with 105877 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
# CI Cache Optimization Research
> Last CI run analyzed: [#5989](https://github.com/Chainlit/chainlit/actions/runs/24017813922) on `feat/parallel-cypress` (2026-04-06, success, 13m 36s wall clock)
## Timing Findings
### Critical Path
The pipeline bottleneck is **Windows E2E shards** (13m 12s worst case). 60% of that time is setup, not tests.
```
e2e-tests / windows-latest-2 (13m 12s) ← CRITICAL PATH
┌─ Install Node/pnpm ──── 1m 57s ─┐
│ Install Cypress ─────── 1m 33s │
│ Install Python/uv ──── 2m 50s │ 7m 49s setup (60%)
│ Build UI ────────────── 1m 29s │
└──────────────────────────────────┘
┌─ Run tests ───────────── 4m 19s ─┐ 4m 19s actual work
└──────────────────────────────────┘
```
### All Jobs (wall clock)
| Job | Total | pnpm install | Cypress | uv sync | Build UI | Tests |
| ---------------------- | ---------- | ------------ | ------- | ---------- | -------- | -------- |
| lint-backend | 1m 21s | — | — | 11s | — | mypy 58s |
| lint-ui | 1m 52s | 30s | — | — | 1m 07s | lint 10s |
| pytest (3.10) | 3m 09s | 33s | — | 58s | 1m 07s | 24s |
| pytest (3.11) | 3m 04s | 33s | — | 56s | 1m 06s | 23s |
| pytest (3.12) | 3m 03s | 30s | — | 53s | 1m 07s | 29s |
| **pytest (3.13)** | **5m 45s** | 30s | — | **3m 40s** | 1m 06s | 25s |
| e2e ubuntu (avg of 5) | ~5m 50s | ~31s | ~11s | ~55s | ~1m 07s | ~3m |
| e2e windows (avg of 5) | ~11m 56s | ~2m 02s | ~1m 30s | ~2m 28s | ~1m 22s | ~3m 50s |
### Anomalies
- **pytest (3.13) uv sync: 3m 40s** — vs ~55s for 3.103.12. Verified from logs: uv cache save **failed** during this run due to a transient GitHub cache service outage (`"Our services aren't available right now"`). The 3.13 slowness is likely source-building wheels that lack pre-built binaries for Python 3.13. This is a one-time cost that gets cached naturally once the service recovers — not a config issue.
- **Windows pnpm install: ~2m** — vs ~30s on Ubuntu. Disk I/O and process spawning overhead.
- **uv cache save failed across all jobs** in this run — transient outage, not a config problem.
### Redundant Work
`pnpm run buildUi` (builds `libs/react-client``libs/copilot``frontend`) runs independently in **7 jobs**:
- 2 E2E jobs (ubuntu + windows) × ~1m 10s = ~2m 20s
- 4 pytest jobs × ~1m 07s = ~4m 28s
- 1 lint-ui job × ~1m 07s = ~1m 07s
- **Total: ~8 min of cumulative runner time** rebuilding identical, OS-independent output
Note: On `feat/refactor-scripts`, the Hatch build hook is already bypassed — `uv-python-install` uses `--no-install-project --no-editable`, and `pnpm run buildUi` is an explicit step. The `lint-backend` workflow does **not** need the build.
---
## Current Cache Configuration
| Tool | Action | Cache mechanism | Status |
| ------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------- |
| pnpm store | `actions/setup-node@v6.3.0` | `cache: 'pnpm'`, `cache-dependency-path: '**/pnpm-lock.yaml'` | ✅ Working (30s installs on Ubuntu) |
| uv download cache | `astral-sh/setup-uv@v8.0.0` | `enable-cache: true` | ✅ Working (55s on Ubuntu), ⚠️ cache miss on 3.13 |
| Cypress binary | `cypress-io/github-action@v6` (run used v6 despite local file referencing v7.1.9) | Internal (built-in to the action) | ✅ Ubuntu (11s), ⚠️ Windows (90s) |
| Python installation | `actions/setup-python@v6.2.0` | Built-in (pre-installed on runners) | ✅ Fast on Ubuntu |
| UI build output | — | **Not cached** | ❌ Rebuilt 14× per run |
---
## Research: Recommended Approaches per Tool
### 1. pnpm — `actions/setup-node` and `pnpm/action-setup`
**Source**: [actions/setup-node README](https://github.com/actions/setup-node), [pnpm/action-setup README](https://github.com/pnpm/action-setup)
**What's recommended**:
- `actions/setup-node` with `cache: 'pnpm'` — caches pnpm's content-addressable store. This is **already in use** and is the recommended approach.
- `pnpm/action-setup` also offers `cache: true` but using both is redundant.
- Docs explicitly recommend committing `pnpm-lock.yaml` and using `--frozen-lockfile` in CI.
**What's NOT recommended**:
- Caching `node_modules/` directly. pnpm uses hard-linked content-addressable storage; caching `node_modules` breaks this model and can cause integrity issues.
**Assessment**: Current config is correct. No change needed for pnpm caching itself.
### 2. Cypress binary — `cypress-io/github-action`
**Source**: [cypress-io/github-action README — "Install Cypress only"](https://github.com/cypress-io/github-action#install-cypress-only)
**Recommended pattern** (from the README, section "Install Cypress only"):
```yaml
- uses: actions/cache@v5
with:
path: |
~/.cache/Cypress
node_modules
key: my-cache-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm i cypress
- uses: cypress-io/github-action@v7
with:
install: false
```
**Key observations**:
1. The README shows explicit `actions/cache` for `~/.cache/Cypress` as the recommended approach when splitting install from test execution.
2. The `cypress-io/github-action` has a `cache-key` input for custom cache keys.
3. For pnpm projects, the README shows: install pnpm → setup-node with `cache: 'pnpm'` → let the Cypress action handle installation.
**Verified from CI logs (Ubuntu shard 1)**:
1. `pnpm install` (03:46:4103:47:00) runs Cypress's `postinstall` hook, which **fully installs the binary** in ~9s:
```
cypress@14.5.3 postinstall: Installing Cypress (version: 14.5.3)
cypress@14.5.3 postinstall: Done
```
2. `cypress-io/github-action@v6` with `runTests: false` (03:47:0003:47:11, ~11s) then runs again. Its only log output: `"Skipping running tests: runTests parameter is false"`. It re-runs the install/verify (redundant) and saves the Cypress binary cache in its post-step.
**Conclusion**: The action is **redundant for installation**. Its only value is the **post-step cache save** of `~/.cache/Cypress`. This can be replaced with a plain `actions/cache` step that:
1. Restores `~/.cache/Cypress` BEFORE `pnpm install` (so postinstall skips the download)
2. Saves it after the job completes
This follows the "Install Cypress only" pattern from the action's own README.
**Paths for Windows**: The `~/.cache/Cypress` path is Linux-only. Windows uses `~/AppData/Local/Cypress/Cache`. Both must be listed in the `actions/cache` path, OR use the `CYPRESS_CACHE_FOLDER` env var to standardize.
### 3. uv — `astral-sh/setup-uv`
**Source**: [setup-uv README](https://github.com/astral-sh/setup-uv), [uv GitHub Actions guide](https://github.com/astral-sh/uv/blob/main/docs/guides/integration/github.md), [uv caching concepts](https://github.com/astral-sh/uv/blob/main/docs/concepts/cache.md)
**What's recommended**:
- `enable-cache: true` — caches uv's download cache (wheels, sdists). **Already in use.**
- `cache-python: true` — caches Python installations managed by uv. **Not currently used.** This would help when uv installs Python (vs `actions/setup-python`).
- `cache-dependency-glob` — controls cache key. Defaults to `**/uv.lock` etc. Current config uses defaults.
- Manual approach: `actions/cache` with `UV_CACHE_DIR` + `uv cache prune --ci` for more control.
**What's NOT recommended**:
- Caching `.venv/` directly. The uv docs don't mention or recommend this. uv is designed to be fast at resolving+linking from its cache.
- Caching pre-built wheels. From the uv docs: _"it's often faster to omit pre-built wheels from the cache (and instead re-download them from the registry on each run)."_ Only source-built wheels benefit from caching.
**Verified from CI logs**: The 3.13 outlier coincided with a **transient GitHub cache service outage** (`"Failed to save: Our services aren't available right now"` in the post-step). All pytest jobs in this run failed to save their uv cache. The 3m40s `uv sync` is likely source-building wheels that don't have pre-built binaries for Python 3.13 yet, combined with the cache miss.
**Assessment**: `cache-python: true` would NOT help here — it caches the Python installation binary, not package wheels. The uv download cache (`enable-cache: true`, already in use) is the correct mechanism; it just failed to save in this run due to the outage. No config change needed — this resolves on the next successful cache save.
**Important from uv docs**: _"it's often faster to omit pre-built wheels from the cache (and instead re-download them from the registry on each run)."_ Only source-built wheels benefit from caching. The current `enable-cache: true` already handles this correctly.
### 4. UI build output — no standard tool
**Source**: No specific recommendation from any action. This is a custom optimization.
**Standard GitHub Actions patterns**:
1. **`actions/cache`** — Cache build output keyed on source hash. Pros: simple. Cons: 10GB cache limit shared across all branches; large build outputs can evict other caches.
2. **`actions/upload-artifact` + `actions/download-artifact`** — Build once in a dedicated job, share artifacts with downstream jobs. Pros: no cache eviction issues, guaranteed fresh builds. Cons: adds a sequential dependency (build job must complete before shards start), artifact upload/download overhead.
**Assessment**: A dedicated build job with artifact sharing is the cleaner pattern for this case (14 consumers, deterministic output). But it changes the dependency graph — E2E and pytest jobs would need to wait for the build job.
---
## Proposed Changes
### P1: Replace `cypress-io/github-action` with `actions/cache` for Cypress binary
**Status**: Verified from logs — the action is redundant for installation.
**Why `install: false` + `runTests: false` doesn't help**: The action's cache restore happens in its **main step**, which runs AFTER `pnpm-node-install`. By then, `pnpm install` has already triggered Cypress's `postinstall` hook and downloaded the binary. The cache restore is too late. With both flags off, the action is effectively a no-op during the main step — it only saves the cache in its post-step for future runs.
The README's [`install: false` pattern](https://github.com/cypress-io/github-action#install-cypress-only) assumes the action is still used to **run tests**. We don't — we use `pnpm test:e2e` with `cypress-split`.
**Approach**: Place `actions/cache` **before** `pnpm install` so the Cypress binary is restored before the postinstall hook runs:
1. Add `actions/cache` for Cypress binary dir BEFORE `pnpm-node-install`
2. Remove the `cypress-io/github-action` step entirely
3. Cypress's `postinstall` hook skips download when it finds the binary pre-cached
```yaml
# In e2e-tests.yaml, BEFORE pnpm-node-install:
- name: Cache Cypress binary
uses: actions/cache@v4
with:
path: |
~/.cache/Cypress
~/AppData/Local/Cypress/Cache
key: cypress-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
# Then remove the cypress-io/github-action step entirely.
```
**Cypress version**: `package.json` specifies `^14.5.3`, `pnpm-lock.yaml` resolves to `14.5.4`. Cypress is installed by `pnpm install` (postinstall hook) in both the current and proposed flow — no version change.
**Expected critical-path saving**: ~1m 30s per Windows shard (eliminates the redundant action step AND speeds up postinstall via cache hit on warm cache).
### P2: Build UI once at CI startup, share via cache
**Status**: Primary optimization — eliminates ~1m build per downstream job (×7 jobs).
**Problem**: `pnpm run buildUi` runs independently in 7 jobs, producing identical OS-independent output each time. Without a dedicated build job, all parallel jobs miss the cache simultaneously on the first run and all rebuild.
**Build command**: `pnpm run buildUi` = `pnpm build:libs && cd frontend && pnpm run build`
**Build output directories**:
- `libs/react-client/dist/`
- `libs/copilot/dist/`
- `frontend/dist/`
**Approach**: Add a `build-ui` job in `ci.yaml` that builds once and saves to `actions/cache`. Downstream workflows restore from cache and skip the build.
**Cache key**: `ui-build-${{ github.sha }}`
Using the commit SHA is the simplest correct key. Enumerating source files in `hashFiles()` would be fragile — the build depends on `package.json`, `pnpm-lock.yaml`, `vite.config.*`, `tsconfig.*`, `index.html`, plus all source in `frontend/src/`, `libs/react-client/src/`, `libs/copilot/src/`. Too many inputs to track reliably. The trade-off: `github.sha` rebuilds on every new commit even if only backend files changed. But on cache hit the `build-ui` job is ~15s (restore only), so the wasted work is minimal. `github.run_id` would NOT work — it's unique per run, so the cache would never hit across runs (e.g. retries).
```yaml
# ci.yaml
jobs:
build-ui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/pnpm-node-install
- uses: actions/cache@v4
id: ui-cache
with:
path: |
frontend/dist
libs/react-client/dist
libs/copilot/dist
key: ui-build-${{ github.sha }}
- name: Build UI
if: steps.ui-cache.outputs.cache-hit != 'true'
run: pnpm run buildUi
pytest:
needs: build-ui # wait for build
uses: ./.github/workflows/pytest.yaml
e2e-tests:
needs: build-ui # wait for build
uses: ./.github/workflows/e2e-tests.yaml
lint-ui:
needs: build-ui # wait for build
uses: ./.github/workflows/lint-ui.yaml
lint-backend: # no build needed — runs in parallel with build-ui
uses: ./.github/workflows/lint-backend.yaml
```
Downstream workflows restore from cache and skip `pnpm run buildUi`:
```yaml
# In pytest.yaml, e2e-tests.yaml, lint-ui.yaml — replace the buildUi step:
- uses: actions/cache@v4
with:
path: |
frontend/dist
libs/react-client/dist
libs/copilot/dist
key: ui-build-${{ github.sha }}
fail-on-cache-miss: true # build-ui already ran, cache must exist
```
**Why cache over artifacts?**:
- **Artifacts**: free for public repos (no storage cap), but re-upload every run even when nothing changed. Each run pays upload+download overhead.
- **Cache**: 10 GB per repo (shared with pnpm/uv/Cypress caches), but on repeat runs with the same SHA (retries, re-triggered workflows) the `build-ui` job itself becomes ~15s (cache hit, skip build). Build output is ~50-100 MB, well within limits alongside other caches.
**Why `build-ui` is still needed**: Without it, all parallel downstream jobs would miss the cache simultaneously on the first run and all build independently. `needs: build-ui` guarantees the cache is populated before they start.
**Trade-off**: `build-ui` adds a sequential dependency. On cache miss: ~1m 30s (pnpm install ~30s + build ~1m). On cache hit: ~15s. `lint-backend` runs in parallel and takes ~1m 20s, so `build-ui` doesn't extend the critical path.
**Expected saving**: ~1m per downstream job × 7 jobs = **~7 min cumulative runner time**. Critical path saving: ~1m per Windows E2E job (build eliminated, ~5s cache restore).
### P3: Merge `validate` + `prepare` e2e jobs
**Approach**: Combine the two trivial `ubuntu-slim` jobs into one to eliminate a job startup cycle.
**Expected saving**: ~10-15s.
### ~~P4: Add `cache-python: true` to setup-uv~~ (dropped)
**Reason**: Verified from logs that the Python 3.13 outlier was caused by a transient GitHub cache service outage, not a config issue. `cache-python: true` caches the Python installation binary, which is irrelevant — the slowness came from source-building wheels. The existing `enable-cache: true` already caches built wheels correctly. No change needed.
---
## Estimated Impact on Critical Path
| Change | Current (win E2E) | After | Saving |
| --------------------------------- | ------------------------------------ | ------------------- | ----------- |
| P1: Cypress cache + remove action | 1m 33s (action) + ~30s (postinstall) | ~5s | ~2m |
| P2: Build UI once + cache | ~1m 22s (build per job) | ~5s (cache restore) | ~1m 17s |
| P3: merge validate+prepare | ~15s overhead | 0s | ~15s |
| **Cumulative (critical path)** | **13m 12s** | **~9m 40s** | **~3m 32s** |
Notes:
- P1 and P2 are the most reliable wins. P2 eliminates redundant builds across all 7 consuming jobs (E2E, pytest, lint-ui) with a single approach.
- P2's `build-ui` job adds a sequential dependency (~1m 30s on Ubuntu) but `lint-backend` runs in parallel and takes ~1m 20s, so `build-ui` doesn't extend the critical path.
- Cumulative runner time saved (not just critical path): ~9m across all 7 jobs that currently build UI.
---
## Discarded Ideas (with reasons)
| Idea | Why discarded | Source |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Cache `node_modules/` directly | pnpm uses hard-linked content-addressable storage; caching `node_modules` breaks this model | [pnpm docs](https://pnpm.io/faq#why-is-it-recommended-to-not-use-npm-ci-for-pnpm), [actions/setup-node](https://github.com/actions/setup-node) |
| Cache `.venv/` directly | uv docs don't recommend this; uv is designed to resolve+link fast from its cache | [uv caching docs](https://github.com/astral-sh/uv/blob/main/docs/concepts/cache.md) |
| `cache-python: true` in setup-uv | Wouldn't help — 3.13 outlier was source-building wheels (not Python install), and was caused by a transient cache service outage | Verified from CI logs |
| Cache pre-built wheels | uv docs explicitly say _"it's often faster to omit pre-built wheels from the cache"_ — only source-built wheels benefit | [uv caching docs](https://github.com/astral-sh/uv/blob/main/docs/concepts/cache.md) |
+916
View File
@@ -0,0 +1,916 @@
# Copilot Type Checking — Research & Proposed Solutions
## Problem Statement
Running `tsc --noemit` in `libs/copilot/` fails because copilot **directly imports source files from `@chainlit/app` (the frontend)**, and those source files rely on path aliases/baseUrl settings defined in the frontend's own `tsconfig.json` — settings that are **not available** when TypeScript runs under copilot's `tsconfig.json`.
The goal is to make `pnpm --filter @chainlit/copilot type-check` work correctly with minimal changes to copilot source code and, if possible, `tsconfig.json`.
---
## Architecture Summary
### Monorepo Workspace (pnpm)
```
pnpm-workspace.yaml:
- frontend/ → @chainlit/app (private, no exports/main/types)
- libs/react-client/ → @chainlit/react-client (published, has main/types → dist/)
- libs/copilot/ → @chainlit/copilot (private)
```
### Dependency Graph
```
@chainlit/copilot
├── @chainlit/app (workspace:^) ← imports SOURCE files
└── @chainlit/react-client (workspace:^) ← imports from dist/ (proper library)
```
### How Copilot Imports from `@chainlit/app`
Copilot reaches **directly into frontend source** via deep path imports:
```typescript
// libs/copilot/src/chat/body.tsx
import Alert from '@chainlit/app/src/components/Alert';
import ChatSettingsModal from '@chainlit/app/src/components/ChatSettings';
import { ErrorBoundary } from '@chainlit/app/src/components/ErrorBoundary';
import { TaskList } from '@chainlit/app/src/components/Tasklist';
import ChatFooter from '@chainlit/app/src/components/chat/Footer';
import MessagesContainer from '@chainlit/app/src/components/chat/MessagesContainer';
import ScrollContainer from '@chainlit/app/src/components/chat/ScrollContainer';
import Translator from '@chainlit/app/src/components/i18n/Translator';
import { useLayoutMaxWidth } from '@chainlit/app/src/hooks/useLayoutMaxWidth';
import { useUpload } from '@chainlit/app/src/hooks/useUpload';
import { IAttachment, attachmentsState } from '@chainlit/app/src/state/chat';
```
Full list of imported modules across all copilot files (25 import lines, 22 unique paths, 7 files):
| Copilot File | Imported from `@chainlit/app/src/...` |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `widget.tsx` | `components/Alert`, `components/ui/button`, `components/ui/popover` |
| `components/WelcomeScreen.tsx` | `components/chat/Starters`, `lib/utils` |
| `components/Header.tsx` | `components/AudioPresence`, `components/Logo`, `components/header/ChatProfiles`, `components/header/NewChat`, `components/ui/button` |
| `components/ElementSideView.tsx` | `components/Elements`, `components/ui/dialog` |
| `appWrapper.tsx` | `i18n` |
| `app.tsx` | `components/i18n/Translator` |
| `chat/body.tsx` | `components/Alert`, `components/ChatSettings`, `components/ErrorBoundary`, `components/Tasklist`, `components/chat/Footer`, `components/chat/MessagesContainer`, `components/chat/ScrollContainer`, `components/i18n/Translator`, `hooks/useLayoutMaxWidth`, `hooks/useUpload`, `state/chat` |
### Import Categories
Not just UI components — copilot imports 6 distinct categories:
| Category | Count | Imports |
| ------------------------ | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **UI components** | 13 | `Alert`, `AudioPresence`, `ChatSettings`, `Elements`, `ErrorBoundary`, `Logo`, `Tasklist`, `Starters`, `button`, `dialog`, `popover`, `ChatProfiles`, `NewChat` |
| **Composite chat views** | 4 | `Footer`, `MessagesContainer`, `ScrollContainer`, `Translator` |
| **Hooks** | 2 | `useLayoutMaxWidth`, `useUpload` |
| **Recoil state** | 1 | `state/chat` (`IAttachment`, `attachmentsState`) |
| **Utilities** | 1 | `lib/utils` (`cn`, `hasMessage`) |
| **i18n config** | 1 | `i18n` (`i18nSetupLocalization`) |
### Contrasted with `@chainlit/react-client` (works correctly)
`react-client` is a properly published library:
- `package.json` has `"main": "dist/index.js"` and `"types": "dist/index.d.ts"`
- Built with `tsup`, emits `.d.ts` declarations
- Copilot imports from the package root: `import { useConfig } from '@chainlit/react-client'`
---
## How Copilot Is Used
Copilot is **NOT a published npm package**. It is a **self-contained IIFE bundle** that gets embedded into the Python backend and served as a drop-in embeddable widget.
### Build & Distribution Flow
```
libs/copilot/
index.tsx ← Vite entry point
vite.config.ts ← builds as IIFE format (not ESM/CJS library)
↓ vite build (rollup, inlineDynamicImports: true)
dist/
index.js ← 8.5 MB single IIFE bundle (everything inlined)
assets/ ← static assets (pdf.worker.min.mjs ~1 MB)
↓ backend/build.py:copy_copilot()
backend/chainlit/copilot/dist/
index.js ← copied into the Python package
↓ FastAPI serves at /copilot/*
End user loads <script src="https://your-chainlit-app/copilot/index.js">
↓ IIFE self-executes
window.mountChainlitWidget(config) ← injects Shadow DOM chat widget
```
### Key Properties
- **Format**: IIFE with `inlineDynamicImports: true` — no code splitting possible
- **Rendering**: Uses Shadow DOM (`attachShadow({ mode: 'open' })`) with inlined CSS
- **Global API**: Exposes `window.mountChainlitWidget()`, `window.unmountChainlitWidget()`, `window.sendChainlitMessage()`, etc.
- **No TypeScript consumers**: Nobody imports from `@chainlit/copilot` — the type-check is purely a development-time quality gate
- **Vite resolves successfully**: The build uses `vite-tsconfig-paths` plugin + explicit `resolve.alias` entries, so the IIFE builds correctly despite tsc failing
### Vite Config Resolution
```typescript
// libs/copilot/vite.config.ts
resolve: {
alias: {
react: path.resolve(__dirname, './node_modules/react'),
'@chainlit': path.resolve(__dirname, './node_modules/@chainlit'),
postcss: path.resolve(__dirname, './node_modules/postcss'),
tailwindcss: path.resolve(__dirname, './node_modules/tailwindcss'),
i18next: path.resolve(__dirname, './node_modules/i18next'),
sonner: path.resolve(__dirname, './node_modules/sonner'),
'highlight.js': path.resolve(__dirname, './node_modules/highlight.js'),
'react-i18next': path.resolve(__dirname, './node_modules/react-i18next'),
'usehooks-ts': path.resolve(__dirname, './node_modules/usehooks-ts'),
lodash: path.resolve(__dirname, './node_modules/lodash'),
recoil: path.resolve(__dirname, './node_modules/recoil')
}
}
```
The `@chainlit` alias points to `node_modules/@chainlit`, which pnpm symlinks:
```
libs/copilot/node_modules/@chainlit/app → ../../../../frontend
libs/copilot/node_modules/@chainlit/react-client → ../../../react-client
```
This means Vite follows symlinks into the actual source directories — `@chainlit/app/src/components/Alert` resolves to `frontend/src/components/Alert.tsx` via filesystem. Vite resolves each file's imports independently using the `vite-tsconfig-paths` plugin, which can apply different tsconfig settings per file. **tsc cannot do this** — it uses a single tsconfig for all files in its program.
---
## Actual Error Analysis (Verified)
Running `pnpm --filter @chainlit/copilot type-check` produces **243 errors** across **35 unique frontend source files**.
### Error Categories
| Category | Example Error | Count | Root Cause |
| --------------------------------- | -------------------------------------------------------------- | ----- | ------------------------------------------------------------------------------- |
| **`@/` alias misresolution** | `Cannot find module '@/components/ui/button'` | ~180 | Copilot's `@/*``./src/*` resolves to `libs/copilot/src/` not `frontend/src/` |
| **`@/` false match** | `Module '"@/types"' has no exported member 'IInput'` | ~20 | Same-named file exists in copilot's src with different exports |
| **Bare `baseUrl` imports** | `Cannot find module 'components/i18n/Translator'` | ~40 | Frontend's `baseUrl: "./src"` doesn't apply under copilot's tsconfig |
| **`client-types/` alias missing** | `Cannot find module 'client-types/*'` | 3 | Copilot has no mapping for frontend's `client-types/*` path |
| **Vite-specific import** | `Cannot find module 'pdfjs-dist/build/pdf.worker.min.mjs?url'` | 1 | tsc doesn't understand Vite's `?url` import suffix |
### Top Missing Modules (by frequency)
```
19× @/components/ui/button
13× @/components/ui/tooltip
6× @/components/ui/dialog
6× @/components/ui/command
5× @/components/ui/skeleton
5× @/components/ui/badge
5× components/i18n (bare import)
4× @/components/ui/select
4× @/components/ui/input
4× @/components/Markdown
4× @/components/Icon
4× components/i18n/Translator (bare import)
3× types/Input (bare import)
3× @/state/chat
3× client-types/ (missing alias)
2× hooks/useFetch (bare import)
```
### All Bare Module Imports (non-`@/`, non-`client-types/`)
These rely on `baseUrl` resolution and **cannot** be fixed via `paths` mappings alone:
```
components/i18n
components/i18n/Translator
contexts/MessageContext
hooks/useFetch
state/chat
state/project
types/Input
```
### Transitive Fan-Out
The 22 direct imports from copilot fan out into 35 unique frontend files with errors. The heaviest fan-out comes from three entry points:
1. **`ChatSettings`** → pulls in 12 sub-components (`CheckboxInput`, `SelectInput`, `DatePickerInput`, `MultiSelectInput`, `RadioButtonGroup`, `SliderInput`, `SwitchInput`, `TagsInput`, `TextInput`, `FormInput`, `InputStateHandler`) each importing 3-6 UI primitives
2. **`Elements`** → pulls in `Plotly.tsx`, `PDF.tsx`, `LazyDataframe.tsx`, `CustomElement/`, `File.tsx`, `Image.tsx`, `Text.tsx`
3. **`MessagesContainer`** → pulls in `MessageComposer/` (10+ subfiles: `Input`, `SubmitButton`, `UploadButton`, `VoiceButton`, `ModePicker`, `CommandButtons`, `Mcp/`, etc.)
---
## Root Cause Analysis
### The Core Problem: Cross-Project Path Alias Mismatch
When `tsc` runs with `libs/copilot/tsconfig.json`, it follows imports into `frontend/src/...` files. Those files are type-checked using copilot's path mappings, not frontend's. TypeScript has **no mechanism to apply different tsconfig settings to different source files within a single compilation**.
#### Copilot's tsconfig.json
```json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": { "@/*": ["./*"] },
"moduleResolution": "Node",
"skipLibCheck": true,
"noEmit": true
},
"include": ["./src"]
}
```
#### Frontend's tsconfig.json
```json
{
"compilerOptions": {
"composite": true,
"baseUrl": "./src",
"paths": {
"client-types/*": ["../../libs/react-client/dist"],
"@/*": ["./*"]
},
"moduleResolution": "Node",
"noEmit": true
},
"include": ["./src"]
}
```
#### Three categories of internal imports that fail:
1. **`@/` Path Alias** (130+ frontend files use this)
- Frontend: `@/lib/utils``frontend/src/lib/utils`
- Under copilot's tsconfig: `@/lib/utils``libs/copilot/src/lib/utils` (WRONG)
2. **`client-types/` Path Alias** (8 frontend files)
- Frontend: `client-types/*``../../libs/react-client/dist`
- Under copilot's tsconfig: not mapped → module not found
3. **Bare Module Imports via `baseUrl`** (33+ frontend files)
- Frontend: `components/Alert``frontend/src/components/Alert` (via `baseUrl: "./src"`)
- Under copilot's tsconfig: `components/Alert``libs/copilot/src/components/Alert` (WRONG)
### Why Frontend Type-Check Passes
`pnpm --filter @chainlit/app type-check` succeeds because tsc uses the frontend's own tsconfig where all aliases resolve correctly. The problem only manifests when **copilot's** tsc follows imports into frontend files.
### CI Impact
```yaml
# .github/workflows/check-frontend.yaml
- name: 'Type checking: frontend'
run: pnpm type-check # runs: pnpm run --parallel type-check
```
Root `package.json`: `"type-check": "pnpm run --parallel type-check"` — runs all three packages in parallel. Frontend and react-client pass; copilot fails. **CI is currently broken for copilot type-checking.**
---
## Bundle Size Analysis
### Methodology
Bundle analyzed using `rollup-plugin-visualizer` (v7.0.1, March 2026) with treemap template and gzip size calculation, integrated as a Vite plugin during a one-off build.
### Overview
| Metric | Size |
| --------------------------- | --------------------------------- |
| Rendered (pre-minification) | 20.0 MB |
| Minified | 8,551.6 KB (8.3 MB) |
| Gzipped | 2,518.7 KB (2.5 MB) |
| Module count | 3,053 |
| Additional asset | `pdf.worker.min.mjs` — 1,046.2 KB |
### Bundle Composition by Package
```
10900.4 KB (53.2%) plotly.js
1672.1 KB ( 8.2%) highlight.js
938.5 KB ( 4.6%) lucide-react
780.9 KB ( 3.8%) pdfjs-dist
673.3 KB ( 3.3%) lodash
612.9 KB ( 3.0%) katex
459.6 KB ( 2.2%) sucrase
331.6 KB ( 1.6%) date-fns
327.4 KB ( 1.6%) @chainlit/app (frontend source)
281.9 KB ( 1.4%) parse5
268.2 KB ( 1.3%) react-dom
193.2 KB ( 0.9%) recoil
162.0 KB ( 0.8%) other
154.6 KB ( 0.8%) zod
145.9 KB ( 0.7%) tailwind-merge
134.0 KB ( 0.7%) react-day-picker
129.6 KB ( 0.6%) react-player
118.2 KB ( 0.6%) micromark-core-commonmark
112.9 KB ( 0.6%) @tanstack/table-core
99.3 KB ( 0.5%) react-hook-form
94.8 KB ( 0.5%) swr
89.4 KB ( 0.4%) i18next
66.9 KB ( 0.3%) entities
57.3 KB ( 0.3%) react-pdf
49.6 KB ( 0.2%) react-dropzone
48.8 KB ( 0.2%) @radix-ui/react-select
48.4 KB ( 0.2%) embla-carousel
48.2 KB ( 0.2%) engine.io-client
42.0 KB ( 0.2%) @chainlit/react-client
```
### Top 30 Individual Modules
```
1. 10900.3 KB plotly.js/dist/plotly.js
2. 780.9 KB pdfjs-dist/build/pdf.mjs
3. 612.9 KB katex/dist/katex.mjs
4. 562.8 KB lodash/lodash.js
5. 193.2 KB recoil/es/index.js
6. 159.0 KB highlight.js/lib/languages/mathematica.js
7. 154.6 KB zod/lib/index.mjs
8. 136.2 KB react-dom/cjs/react-dom.production.min.js (copilot copy)
9. 130.8 KB react-dom/cjs/react-dom.production.min.js (frontend copy)
10. 112.9 KB @tanstack/table-core/build/lib/index.mjs
11. 111.8 KB highlight.js/lib/languages/isbl.js
12. 109.6 KB parse5/dist/tokenizer/index.js
13. 107.1 KB parse5/dist/parser/index.js
14. 99.3 KB react-hook-form/dist/index.esm.mjs
15. 89.4 KB i18next/dist/esm/i18next.js
16. 82.2 KB highlight.js/lib/languages/gml.js
17. 80.4 KB copilot src/index.css (inlined Tailwind CSS)
18. 76.4 KB highlight.js/lib/core.js
19. 73.2 KB tailwind-merge (copilot copy)
20. 72.8 KB tailwind-merge (frontend copy)
21. 64.5 KB highlight.js/lib/languages/1c.js
22. 62.7 KB highlight.js/lib/languages/sqf.js
23. 48.8 KB @radix-ui/react-select/dist/index.mjs
24. 48.4 KB embla-carousel/esm/embla-carousel.esm.js
25. 47.1 KB sucrase/dist/esm/parser/plugins/typescript.js
26. 46.6 KB entities/lib/esm/generated/decode-data-html.js
27. 43.0 KB sucrase/dist/esm/parser/tokenizer/readWordTree.js
28. 42.0 KB @chainlit/react-client/dist/index.mjs
29. 37.2 KB react-dropzone/dist/es/index.js
30. 34.1 KB @radix-ui/react-menu/dist/index.mjs
```
### Dependency Origin Split
| Source | Size | % |
| -------------------------------------- | ----------- | ----- |
| Frontend's `node_modules` (transitive) | 16,952.9 KB | 82.7% |
| Copilot's own `node_modules` (direct) | 3,554.7 KB | 17.3% |
**82.7% of the copilot bundle comes from frontend's transitive dependencies.**
### Duplicate Packages
Several packages are bundled **twice** — once resolved from `frontend/node_modules` and once from `libs/copilot/node_modules` — because pnpm's strict module isolation creates separate copies:
| Package | Frontend copy | Copilot copy | Waste |
| ---------------------------- | ------------- | ------------ | ----------- |
| react-dom | 131.4 KB | 136.9 KB | 131.4 KB |
| tailwind-merge | 72.8 KB | 73.2 KB | 72.8 KB |
| swr | 46.4 KB | 48.5 KB | 46.4 KB |
| lucide-react | 934.6 KB | 3.9 KB | 3.9 KB |
| scheduler | 4.4 KB | 4.4 KB | 4.4 KB |
| uuid | 2.3 KB | 4.8 KB | 2.3 KB |
| **Total duplicate overhead** | | | **~263 KB** |
Note: The `vite.config.ts` `resolve.alias` entries try to deduplicate `react`, `react-dom`, `recoil`, etc. but don't cover all packages (notably `tailwind-merge`, `swr` are missing).
### Packages Copilot Probably Doesn't Need
These are pulled in transitively via `Elements`, `ChatSettings`, and `MessagesContainer` and may not be necessary for a widget:
| Package | Size | Pulled in by |
| -------------------- | -------------- | ------------------------------------------------------- |
| plotly.js | 10,900 KB | `Elements``Plotly.tsx``react-plotly.js` |
| pdfjs-dist | 781 KB | `Elements``PDF.tsx``react-pdf` |
| katex | 613 KB | `MessagesContainer` → Markdown → `rehype-katex` |
| sucrase | 460 KB | Tailwind CSS (runtime processor?) |
| date-fns | 332 KB | `ChatSettings``DatePickerInput``react-day-picker` |
| parse5 | 282 KB | Markdown → `rehype-raw` (HTML parser) |
| zod | 155 KB | Frontend form validation |
| @tanstack/table-core | 113 KB | `Elements``LazyDataframe` |
| react-hook-form | 99 KB | `ChatSettings` form management |
| react-player | 130 KB | `Elements` → video player |
| embla-carousel | 48 KB | `ChatSettings` → carousel component |
| **Subtotal** | **~13,913 KB** | **67.9% of total bundle** |
---
## Proposed Solutions
### Solution 1: Add Frontend Path Mappings to Copilot's tsconfig
**Approach**: Duplicate the frontend's path mappings in copilot's tsconfig, adjusted for the relative directory difference.
```jsonc
// libs/copilot/tsconfig.json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": [
"./*", // copilot's own @/ imports
"../../frontend/src/*" // frontend's @/ imports
],
"client-types/*": [
"../../libs/react-client/dist/*" // frontend's client-types alias
]
}
}
}
```
**Pros**:
- Minimal change (only `tsconfig.json`)
- No source code changes
- Quick to implement
**Cons**:
- `@/*` becomes ambiguous — TypeScript tries copilot path first, then frontend path; potential false resolution if both have a file with the same name
- **Does NOT solve bare module imports** (`components/...`, `hooks/...`) because those rely on `baseUrl` and there's only one `baseUrl` per tsconfig (~40 errors remain)
- Fragile: any new path alias in frontend must be duplicated in copilot
- Couples copilot's tsconfig to frontend's internal structure
**Verdict**: Partial fix (~75% of errors). Handles `@/` and `client-types/` but not bare imports.
---
### Solution 2: TypeScript Project References (`tsc -b`)
**Approach**: Use TypeScript's composite project references so that copilot references the frontend project. The frontend would emit `.d.ts` declarations that copilot consumes.
```jsonc
// libs/copilot/tsconfig.json
{
"references": [
{ "path": "../../frontend" },
{ "path": "./tsconfig.node.json" }
]
}
```
Frontend tsconfig already has `"composite": true`. Would need to remove `"noEmit": true` from frontend and configure `declarationDir`.
**Pros**:
- Architecturally correct TypeScript solution
- Each project uses its own tsconfig
- Incremental builds
**Cons**:
- **Does not help here** because copilot doesn't import from `@chainlit/app` (the package root) — it imports from `@chainlit/app/src/components/...` (deep source paths). Project references resolve the _package root_ to declaration output, not source sub-paths.
- Requires frontend to emit declarations (non-trivial for a complex app)
- Would need the frontend to define an `exports` map or copilot to change all import paths
**Verdict**: Only works if combined with changing import paths (see Solution 5 or 6).
---
### Solution 3: Separate Type-Check tsconfig for Copilot
**Approach**: Create a `tsconfig.typecheck.json` in copilot that extends the base config but adjusts resolution specifically for type-checking. Use `rootDirs` to create a virtual merged directory.
```jsonc
// libs/copilot/tsconfig.typecheck.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDirs": ["./src", "../../frontend/src"],
"paths": {
"@/*": ["./*", "../../frontend/src/*"],
"client-types/*": ["../../libs/react-client/dist/*"]
}
}
}
```
Update `package.json`:
```json
"type-check": "tsc --noemit -p tsconfig.typecheck.json"
```
**Pros**:
- No changes to production tsconfig (IDE, Vite still use `tsconfig.json`)
- Can be as hacky as needed without affecting build
- `rootDirs` can help with bare module resolution
**Cons**:
- `rootDirs` doesn't change module resolution — it only affects output directory structure and relative import resolution between the listed roots. It won't make `components/Alert` resolve to `frontend/src/components/Alert`.
- Still can't solve bare imports via `baseUrl` (~40 errors remain)
- Extra config file to maintain
**Verdict**: Marginal improvement over Solution 1. Keeps production config clean but same fundamental limits.
---
### Solution 4: Frontend Barrel Exports (Source-Level)
**Approach**: Have the frontend expose an explicit public API that copilot imports, instead of reaching into source files.
Create `frontend/src/copilot-exports.ts`:
```typescript
// Explicit re-exports for copilot consumption
export { default as Alert } from './components/Alert';
export { Button } from './components/ui/button';
export {
Popover,
PopoverContent,
PopoverTrigger
} from './components/ui/popover';
export { default as Starters } from './components/chat/Starters';
export { cn, hasMessage } from './lib/utils';
// ... all other components copilot needs
```
Add to `frontend/package.json`:
```json
{
"exports": {
"./copilot": "./src/copilot-exports.ts"
}
}
```
Then copilot imports:
```typescript
import { Alert, Button, cn } from '@chainlit/app/copilot';
```
**Pros**:
- Clean API boundary
- Frontend controls what's public
- Single file to maintain
- tsc resolves it through one entry point
**Cons**:
- **Does NOT solve the transitive resolution problem**: tsc still follows imports inside `copilot-exports.ts` into the rest of the frontend source, hitting the same path alias issues in all 35 downstream files
- Requires changing copilot import paths (moderate source changes)
- Must keep the barrel file in sync
**Verdict**: Doesn't solve the root cause unless combined with declaration generation.
---
### Solution 5: Build Frontend Declarations (tsup) + Barrel Export
**Approach**: Build the frontend components that copilot needs into a declaration file (`.d.ts`) using tsup. Copilot consumes built type artifacts rather than source. This follows the same proven pattern as `@chainlit/react-client`.
#### Implementation
**1. Barrel file**`frontend/src/copilot-exports.ts` (22 re-export lines):
```typescript
export { default as Alert } from './components/Alert';
export { default as ChatSettingsModal } from './components/ChatSettings';
export { ErrorBoundary } from './components/ErrorBoundary';
export { TaskList } from './components/Tasklist';
export { default as ChatFooter } from './components/chat/Footer';
export { default as MessagesContainer } from './components/chat/MessagesContainer';
export { default as ScrollContainer } from './components/chat/ScrollContainer';
export {
default as Translator,
useTranslation
} from './components/i18n/Translator';
export { default as Starters } from './components/chat/Starters';
export { Element } from './components/Elements';
export { default as AudioPresence } from './components/AudioPresence';
export { Logo } from './components/Logo';
export { default as ChatProfiles } from './components/header/ChatProfiles';
export { default as NewChatButton } from './components/header/NewChat';
export { Button } from './components/ui/button';
export {
Dialog,
DialogContent,
DialogHeader,
DialogTitle
} from './components/ui/dialog';
export {
Popover,
PopoverContent,
PopoverTrigger
} from './components/ui/popover';
export { useLayoutMaxWidth } from './hooks/useLayoutMaxWidth';
export { useUpload } from './hooks/useUpload';
export { type IAttachment, attachmentsState } from './state/chat';
export { cn, hasMessage } from './lib/utils';
export { i18nSetupLocalization } from './i18n';
```
**2. Build script**`frontend/package.json`:
```json
{
"scripts": {
"build:copilot-types": "tsup src/copilot-exports.ts --dts-only --outDir copilot-dist"
}
}
```
**3. Path redirect**`libs/copilot/tsconfig.json`:
```jsonc
{
"compilerOptions": {
"paths": {
"@/*": ["./*"],
"@chainlit/app/copilot": [
"../../frontend/copilot-dist/copilot-exports.d.ts"
]
}
}
}
```
**4. Copilot imports** — 25 import lines across 7 files become ~7 consolidated imports:
```typescript
// Before (body.tsx — 11 lines):
import Alert from '@chainlit/app/src/components/Alert';
import ChatSettingsModal from '@chainlit/app/src/components/ChatSettings';
// ... 9 more
// After (body.tsx — 1 line):
import { Alert, ChatSettingsModal, ErrorBoundary, TaskList, ChatFooter,
MessagesContainer, ScrollContainer, Translator, useLayoutMaxWidth,
useUpload, IAttachment, attachmentsState } from '@chainlit/app/copilot';
```
#### Why Barrel + tsup (not multi-entry, not raw tsc)
- **TypeScript has no glob re-export syntax** — `export * from './**/*'` doesn't exist. The barrel must be an explicit list.
- **tsup bundles declarations** (via `rollup-plugin-dts`) — the output `.d.ts` is **self-contained**, all types inlined, no `@/` or `client-types/` imports leak through. Raw `tsc --emitDeclarationOnly` preserves original import paths, so the same alias mismatch would reappear.
- **`moduleResolution: "Node"`** (used by both projects) **ignores `exports` field** in `package.json`. Multi-entry approaches relying on `exports` maps would not work without upgrading `moduleResolution` to `"Node16"`/`"NodeNext"`/`"Bundler"` across the entire monorepo.
- **Single barrel = 1 entry point, 1 output file, 1 path mapping**. Multi-entry would need 22 tsup entry points + 22 path mappings.
**Pros**:
- **Fully solves the problem**: copilot sees `.d.ts` files, no transitive source resolution. `skipLibCheck: true` (already set) skips checking inside declarations.
- Follows the same pattern as `@chainlit/react-client` (proven in this repo)
- Clean architecture with explicit API boundary
- Frontend controls what's public
**Cons**:
- Requires a build step: `pnpm --filter @chainlit/app build:copilot-types` must run before copilot type-check
- ~25 import line changes in 7 copilot source files
- CI pipeline needs ordering
- tsup must handle the transitive dependency tree for `.d.ts` generation — may need config tuning for path aliases
- Must keep barrel file in sync when adding new imports
**Verdict**: Architecturally clean and fully solves the problem. Moderate effort. **Recommended for proper fix.**
---
### Solution 6: Extract `@chainlit/ui` Shared Package
**Approach**: Create a new workspace package `libs/ui/` containing the shared components (Alert, Button, Popover, etc.). Both frontend and copilot depend on it.
```
libs/ui/ → @chainlit/ui
src/
components/
Alert.tsx
ui/button.tsx
ui/popover.tsx
ui/dialog.tsx
...
lib/utils.ts
hooks/useLayoutMaxWidth.ts
hooks/useUpload.tsx
...
package.json (with main/types pointing to dist/)
tsconfig.json
```
**Pros**:
- Architecturally the "right" solution
- Clean dependency graph: `copilot → ui`, `frontend → ui`
- Each package has its own tsconfig, fully independent type checking
- Reusable for future packages
**Cons**:
- **Significant refactoring effort**: need to extract ~25 components/hooks, resolve their dependencies (some depend on `@chainlit/react-client` state atoms, Recoil, etc.)
- Some components (e.g., `MessagesContainer`, `ChatFooter`, `ChatSettings`) are deeply coupled to frontend state — may not be easily extractable
- Would also need to extract hooks (`useUpload`), state atoms (`attachmentsState`), and i18n config
- Risk of circular dependencies
- High risk of breaking changes
**Verdict**: Best long-term architecture but highest implementation cost. Not suitable for a quick fix.
---
### Solution 7: Use `paths` to Redirect `@chainlit/app/src/*` + Eliminate Conflicting Aliases
**Approach**: Instead of copilot's imports going through node_modules, map the entire `@chainlit/app/src/*` prefix in copilot's tsconfig, AND change frontend's internal imports to not conflict with copilot's aliases.
Step 1 — In copilot's tsconfig, map:
```jsonc
{
"paths": {
"@chainlit/app/src/*": ["../../frontend/src/*"],
"@/*": ["./*"]
}
}
```
Step 2 — Change frontend files imported by copilot to use **relative imports** instead of `@/` aliases.
**Pros**:
- Works with existing import structure in copilot (no copilot source changes)
- Relative imports in frontend are more portable
**Cons**:
- Must change 35+ frontend files to use relative imports
- `client-types/` and bare imports still need addressing
- Goes against the frontend's established code style
**Verdict**: Partial solution with significant frontend churn.
---
### Solution 8: Vite-Based Type Checking (`vite-plugin-checker`)
**Approach**: Replace `tsc --noemit` with Vite-integrated type checking.
**Verdict**: **Does not work**`vite-plugin-checker` runs `tsc` under the hood in a worker. It does NOT use Vite's resolver for tsc. Same errors.
---
### Solution 9: Skip Copilot Type-Checking Entirely
**Approach**: Remove copilot from `type-check` or set it to always pass.
```json
"type-check": "echo 'Skipped: cross-project path alias conflict'"
```
**Pros**:
- Zero effort, zero risk
- Vite build already validates copilot compiles correctly
- Copilot has **no downstream TypeScript consumers** — it's an IIFE bundle
- IDE still provides type hints via `vite-tsconfig-paths`
**Cons**:
- Loses compile-time type safety for copilot
- CI won't catch type errors until build step
- Technical debt accumulates silently
**Verdict**: Pragmatic stopgap. More defensible than usual since copilot is a build artifact, not a library.
---
### Solution 10: Frontend Switches to Relative Imports Everywhere
**Approach**: Remove all `@/`, `client-types/`, and bare `baseUrl` imports from the frontend source, replacing with relative imports.
**Pros**:
- **Completely solves the problem** with zero changes to copilot
- Most portable and tool-agnostic approach
**Cons**:
- **170+ files** need changing in frontend
- Long relative paths hurt readability (`../../../../lib/utils`)
- Goes against modern frontend conventions
- High merge conflict risk
**Verdict**: Solves the problem completely but very high cost and DX regression.
---
### Solution 11: Aggressive Path Mapping (All Categories)
**Approach**: Map every import pattern the frontend uses — `@/`, `client-types/`, and all bare module prefixes — in copilot's tsconfig.
```jsonc
// libs/copilot/tsconfig.json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./*", "../../frontend/src/*"],
"client-types/*": ["../../libs/react-client/dist/*"],
"components/*": ["../../frontend/src/components/*"],
"hooks/*": ["../../frontend/src/hooks/*"],
"state/*": ["../../frontend/src/state/*"],
"types/*": ["../../frontend/src/types/*"],
"contexts/*": ["../../frontend/src/contexts/*"]
}
}
}
```
**Pros**:
- Could resolve **all** error categories through tsconfig alone
- No source code changes in either project
- Quick to try
**Cons**:
- **Extremely fragile**: every bare import pattern in frontend must be mapped
- Copilot's own files could be shadowed if names collide
- `@/` dual resolution has silent false-match risk
- Vite-specific `?url` import still fails
- Any new import pattern in frontend breaks copilot's type-check
**Verdict**: Might work for ~95% of errors. Hacky but zero-source-change. Best option for "make CI green fast."
---
## Solution Comparison Matrix
| # | Solution | Copilot Source Changes | Copilot tsconfig Changes | Frontend Changes | Fully Solves? | Effort | Fragility |
| --- | ---------------------------------- | ---------------------- | ------------------------ | ------------------ | --------------------------------- | ----------- | --------- |
| 1 | Add paths to copilot tsconfig | None | Moderate | None | **Partial** (~75%) | Low | Medium |
| 2 | Project references | None | Small | Moderate | **No** (deep imports bypass refs) | Medium | Low |
| 3 | Separate typecheck tsconfig | None | New file | None | **Partial** (~75%) | Low | Medium |
| 4 | Frontend barrel exports (source) | ~15 import changes | None | New barrel file | **No** (transitive issue) | Medium | Low |
| 5 | **Compiled declarations + barrel** | ~25 import lines | Small | Barrel + tsup step | **Yes** | Medium | Low |
| 6 | **Extract `@chainlit/ui`** | ~25 import lines | Small | Major refactor | **Yes** | High | Very Low |
| 7 | Redirect paths + relative imports | None | Small | 35+ files | **Partial** | Medium-High | Medium |
| 8 | Vite-based type checking | None | None | None | **No** | Low | N/A |
| 9 | Skip type-check | Script change | None | None | **N/A** (bypass) | None | N/A |
| 10 | All-relative imports in frontend | None | None | 170+ files | **Yes** | High | Low |
| 11 | **Aggressive path mapping** | None | tsconfig only | None | **~95%** | Low | High |
---
## Recommended Approaches (Ranked)
### Tier 1 — Immediate (make CI green now)
**Solution 11** (aggressive path mapping) or **Solution 9** (skip type-check). These buy time with minimal risk.
### Tier 2 — Proper Fix, Moderate Effort
**Solution 5** (compiled declarations + barrel). Follows the `@chainlit/react-client` pattern already proven in this repo. Creates a clean API boundary. ~25 import line changes in copilot, one barrel file in frontend, one tsup build script.
### Tier 3 — Long-Term Architecture
**Solution 6** (extract `@chainlit/ui` shared package). Do this when the monorepo grows or when copilot needs more independence.
---
## Bundle Optimization Opportunities
Independent of type-checking, the bundle analysis reveals significant optimization potential:
### High Impact (if copilot doesn't need these features)
| Action | Savings (rendered) | Approach |
| ----------------------------------- | ------------------ | -------------------------------------------------------- |
| Exclude plotly.js | ~10,900 KB (53%) | Lazy-load or exclude `Plotly.tsx` from `Elements` barrel |
| Exclude pdfjs-dist | ~781 KB (3.8%) | Lazy-load or exclude `PDF.tsx` from `Elements` barrel |
| Exclude katex | ~613 KB (3.0%) | Use plain markdown without math rendering |
| Exclude date-fns + react-day-picker | ~466 KB (2.3%) | Simplify ChatSettings (no date picker) |
| Exclude sucrase + parse5 | ~742 KB (3.6%) | Review if needed at runtime |
### Medium Impact
| Action | Savings | Approach |
| ----------------------- | -------- | --------------------------------------------------------------------------------- |
| Tree-shake lodash | ~500 KB | `import pick from 'lodash/pick'` instead of `import { pick } from 'lodash'` |
| Deduplicate packages | ~263 KB | Add missing entries to `vite.config.ts` `resolve.alias` (`tailwind-merge`, `swr`) |
| Tree-shake highlight.js | Variable | Import only needed languages instead of all |
### Relationship to Type-Checking Solutions
**Solution 5 (barrel export)** naturally enables bundle optimization: the barrel controls exactly what copilot imports at the type level, and a corresponding change at the Vite level (e.g., re-exporting a lightweight `Element` component that lazy-loads heavy renderers) would reduce bundle size without changing copilot's source code.
**Solution 6 (extract `@chainlit/ui`)** would inherently solve both problems — shared UI components wouldn't carry heavyweight dependencies like plotly.js.
---
## Open Questions / Next Steps
1. **Which tier to pursue?** Quick fix (Solution 11/9), proper fix (Solution 5), or long-term (Solution 6)?
2. **Bundle optimization scope**: Should copilot support Plotly/PDF/video rendering, or should those be frontend-only? This determines whether `Elements` needs to be included as-is or refactored.
3. **CI pipeline ordering**: If Solution 5 is chosen, the `build:copilot-types` step needs to run before `type-check`. Can this be integrated into the existing `pnpm type-check` script or does it need a separate CI step?
4. **`moduleResolution` upgrade**: Is upgrading from `"Node"` to `"Node16"` or `"Bundler"` on the roadmap? This would unlock `exports` map support and simplify cross-package resolution.
+110
View File
@@ -0,0 +1,110 @@
# E2E Test Parallel Execution Research
## Current Architecture: Strictly Sequential
The e2e tests **cannot run in parallel** with the current setup. There are four structural reasons.
### 1. Single Hardcoded Port
Every test backend spawns on port `8000` (`backend/chainlit/config.py`):
```python
DEFAULT_PORT = 8000
```
`runChainlit` in `cypress/support/run.ts` invokes `uv run chainlit run <entryPoint> -h --ci` without passing `--port`, so every backend binds to the default.
### 2. Kill-Restart Lifecycle Per Spec File
`cypress.config.ts` orchestrates a strict **kill → start → test → kill** cycle:
```ts
on('before:spec', async (spec) => {
await killChainlit();
await runChainlit(spec);
});
on('after:spec', async () => {
await killChainlit();
});
```
`killChainlit()` uses `fkill(:8000)` — a port-based kill. If two test runners were active, one would kill the other's backend.
### 3. Each Spec Has Its Own Chainlit App
Each test directory (`cypress/e2e/<test_name>/`) contains:
- `main.py` — the Chainlit application for that test
- `.chainlit/config.toml` — per-test configuration
`runChainlit` uses `spec.absolute` to find the test directory and spawns Chainlit with `CHAINLIT_APP_ROOT` set to that directory. Since each test has a completely different backend app, the server must be restarted between specs.
### 4. Cypress Runs Specs Sequentially
Standard `cypress run` processes spec files one at a time in a single browser. Cypress Cloud offers parallelization across multiple CI machines, but the project doesn't use it (no `--parallel` or `--record` flags in `pnpm test:e2e`).
### Special Case: Mid-Test Restarts
The `data_layer` spec calls `cy.task('restartChainlit', Cypress.spec)` to kill and re-launch the backend within a single test to verify thread persistence across server restarts.
---
## Cypress Parallelization Capabilities
### Built-in `--parallel` Flag Requires Cypress Cloud
Cypress does **not** support parallelization out of the box for free.
```bash
cypress run --record --parallel
```
- `--parallel` **must** be used with `--record`, which sends results to Cypress Cloud (paid).
- Cypress Cloud acts as the orchestrator: it dynamically assigns spec files to available CI machines using a load-balancing strategy based on historical run durations.
- There is no local multi-process parallelism — it is **multi-machine** parallelism coordinated by the cloud service.
### Cypress vs Playwright
| Feature | Cypress | Playwright |
| ----------------- | ------------------------- | ------------------------ |
| Local parallelism | Not supported | Built-in (`--workers=4`) |
| CI sharding | Via Cypress Cloud (paid) | Built-in (`--shard=1/4`) |
| Orchestration | Dynamic, based on history | Manual split |
### Free Alternatives for Cypress Parallelization
| Tool | Description |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| **Manual CI matrix sharding** | Split spec files across GitHub Actions matrix jobs using `--spec` |
| **[sorry-cypress](https://sorry-cypress.dev/)** | Open-source, self-hosted drop-in replacement for Cypress Cloud; supports `--parallel` |
| **[cypress-split](https://github.com/bahmutov/cypress-split)** | Plugin that splits specs across CI machines using `SPLIT`/`SPLIT_INDEX` env vars |
| **[currents.dev](https://currents.dev/)** | Cypress Cloud alternative with a free tier |
---
## What Would It Take to Parallelize
### Bottlenecks and Solutions
| Bottleneck | Solution |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Hardcoded port `8000` | Assign a unique port per worker (e.g. `8000 + workerIndex`). Pass `--port` to `chainlit run` and configure `baseUrl` dynamically. |
| Port-based kill (`fkill(:8000)`) | Switch to PID-based process management — store the child PID from `spawn()` and kill it directly. |
| Single Cypress browser | Use Cypress Cloud `--parallel`, sorry-cypress, cypress-split, or shard tests across CI matrix jobs. |
| Shared filesystem (`.chainlit/` dirs) | Already isolated per test. Some tests (e.g. `data_layer`) write temp files like `thread_history.pickle` that must remain isolated per worker. |
### Strategy A: CI Matrix Sharding (Simplest)
Split the ~50 spec files into N groups across separate GitHub Actions runners:
1. Add a matrix dimension to the workflow that assigns each group to a separate runner.
2. Each runner uses port `8000` (since they are on separate machines).
3. No changes to Cypress config or `run.ts` — pass `--spec` with the subset.
### Strategy B: Same-Machine Parallelism (More Work)
1. Make `CHAINLIT_APP_PORT` dynamic — read from an env var per worker.
2. Replace `fkill(:port)` with PID tracking (`child.pid` from `spawn`).
3. Use `cypress-split` or `sorry-cypress` to distribute specs across multiple Cypress processes, each with its own `baseUrl`.
4. Each Cypress process gets its own `CYPRESS_BASE_URL` pointing to its dedicated backend port.