# PolyLLM 1.0 — "AI operating system" upgrade (2026-09-11)
Massive product/UX/UI/mobile-first upgrade of www.polyllm.io. Goal: a premium, app-like multi-model workspace
("One interface. Every model.") that feels native on iPhone/Android and excellent on desktop, without breaking any
existing backend behaviour (auth, encrypted keys, chat history, Arena, presets, usage, exports, folders, attachments).
This document is the shared contract between the parallel workstreams. Read it fully before touching code.
## Ground rules (all workstreams)
1. **Do not break existing functionality.** Reuse services in `src/lib/*`; extend, don't rewrite. Provider quirks stay in
`src/lib/ai/providers/*`; components only see `PolyModel` capabilities (see `CLAUDE.md`).
2. **Ownership.** Edit only the files/dirs listed for your workstream (plus new files you create there). If you need a change
in someone else's file, add a small **adapter in your own area** and leave a `// TODO(integration: )` comment, or
put the request in `docs/upgrade-notes/.md`. Exception: `src/lib/client/types.ts` — append-only.
3. **Schema.** Tables for the upgrade live in `src/db/schema-workspace.ts` (already migrated: `drizzle/0001_workspace_upgrade.sql`).
If you need more columns/tables: append to `schema-workspace.ts` (or create `src/db/schema-.ts`, re-export it in
`src/db/index.ts`), then run `pnpm db:generate --name ` **and** `pnpm db:migrate` locally. Never edit existing
migration files. Never drop/rename columns.
4. **No dev server in this phase.** Multiple `next dev` in the same tree conflict on `.next`. Validate with
`pnpm typecheck`, `pnpm lint`, `pnpm test` (vitest) and, for server logic, small vitest units in `tests/unit`. Visual QA
happens in the integration phase against a single dev server.
5. **No fake functionality.** Every button works or is explicitly labelled "Coming soon" (use `` badge from
`components/ui/misc` — add it if missing). No placeholder handlers.
6. **Mobile first.** Design for 375 / 390 / 393 / 430 px widths first, then tablet, then desktop. Rules:
- Phones: bottom sheets instead of centered modals (`ResponsiveDialog`, `BottomSheet`, `ActionSheet` in `components/ui/sheet.tsx`),
long-press for message/row actions (`useLongPress`), swipe carousels (`.snap-row` + `useSnapCarousel`), 44 px touch
targets (`.tap` helper or explicit min-h), 16 px inputs, `100dvh`/`.h-app`, safe areas (`pb-safe`, `pt-safe`, `--sab`).
- Never `window.prompt/confirm/alert` — use `PromptDialog` / `ConfirmDialog` (`components/common`).
- No desktop tables overflowing on phones: use stacked rows/cards below `md`, tables from `md` up.
- No horizontal overflow, no clipped text, no fixed element blocking content. Respect `prefers-reduced-motion`.
- Hover-only controls must also be reachable on touch (`.hover-reveal` shows them on coarse pointers).
7. **Design language** (`src/app/globals.css` tokens): neutral warm graphite palette, iris accent, fewer borders, spatial
grouping (`.panel`, hairlines) over boxed cards, restrained 150–300 ms motion, `text-balance` headlines, tabular numbers
for metrics. Use `Segmented`/`ChipRow` (`components/ui/segmented.tsx`) for filters. Icons: lucide only.
8. **Performance.** No new heavy dependencies without a note in your area doc (allowed if < ~40 KB gz and clearly justified —
e.g. `katex` for LaTeX). Virtualize long lists (`@tanstack/react-virtual` is installed). Memoize streaming renders.
9. **Accessibility.** Labels/aria on icon buttons, focus-visible, keyboard nav for lists/palettes, semantic elements.
10. **Commit nothing.** The integrator commits. Leave the tree compiling (`pnpm typecheck && pnpm lint` green) when you finish.
11. **Final report.** End with: files created/changed, new API routes (method, path, body → response), anything left as
"Coming soon", and QA notes (what to check visually) in `docs/upgrade-notes/.md`.
## Shared primitives (already implemented — use them)
| What | Where | Notes |
| --- | --- | --- |
| Tokens, utilities | `src/app/globals.css` | `.h-app`, `.pb-nav`, `.pb-safe`, `.tap`, `.hover-reveal`, `.snap-row`, `.panel`, `.glass-strong`, `.mask-x`, `.marquee`, `coarse:`/`fine:` variants |
| Hooks | `src/lib/client/hooks.ts` | `useIsMobile`, `useIsTablet`, `useIsCoarsePointer`, `useMounted`, `useKeyboardInset`, `useLongPress`, `useSwipe`, `useLocalStorage`, `useDebounced`, `useCopy`, `useSnapCarousel`, `useStandalone` |
| Sheets | `src/components/ui/sheet.tsx` | `BottomSheet` (handle, snap `content|half|full`, swipe-dismiss), `ResponsiveDialog` (sheet on phone, dialog/panel on desktop), `ActionSheet` (iOS-style list) |
| Segmented / chips | `src/components/ui/segmented.tsx` | `Segmented`, `ChipRow` |
| Dialog replacements | `src/components/common/prompt-dialog.tsx`, `confirm-dialog.tsx` | |
| Shell | `src/components/app/shell.tsx`, `bottom-nav.tsx`, `sidebar.tsx`, `store.tsx` | Mobile bottom nav (Chat/Arena/Models/Usage/Account), drawer with edge-swipe, keyboard inset. Store adds `labels`, `setLabel`, `activeProjectId`, `searchOpen`, `AUTO_MODEL_KEY`. |
| Token/cost estimator | `src/lib/client/tokens.ts` | `estimateTextTokens`, `estimateAttachmentTokens`, `estimateContext`, `estimateCost`, `formatEstimate`, `COST_CONFIRM_THRESHOLD_USD` |
| Smart router | `src/lib/client/router.ts` | `analyzePrompt`, `routeModels`, `explainRoute`, `ROUTER_MODES` (pure, transparent) |
| Brand | `src/components/brand/logo.tsx`, `public/icon.svg`, `public/og.png`, `scripts/brand-assets.sh`, `src/app/manifest.ts` | "Prism P" mark; `Wordmark` |
| Schema | `src/db/schema-workspace.ts` | `projects`, `project_files`, `prompts`, `user_model_labels`, `custom_endpoints`, `arena_votes`, `shared_arena_sessions`; `conversations.project_id` |
| Cross-area stubs | `components/projects/sidebar-section.tsx`, `components/library/file-library-picker.tsx` | Baselines; the Projects workstream owns/finishes them. Chat imports `FileLibraryPicker` from that path. |
## Workstreams & ownership
### A — Chat & composer (`components/chat/*` except `model-selector.tsx`, `model-config.tsx`, `model-badges.tsx`; `components/markdown/*`; `app/app/chat/*`; `lib/chat/*`; `app/api/chat/*`; `app/api/attachments/*`)
Mobile composer (+ sheet: upload image/document, camera, paste, from library, tools, web search, structured output, system
prompt, temporary chat), auto-grow 1→6 lines, voice dictation (Web Speech API where available, hidden otherwise), active model
pill above composer (opens the model picker via `ModelSelector` — keep its public props), attachments (drag/drop, paste,
camera, chips, multiple), context indicator (`43K / 200K`, warnings at 80/95 %), cost estimate + confirm above
`COST_CONFIRM_THRESHOLD_USD` with one-tap model alternatives, Smart Router AUTO mode (`selectedModelKey === AUTO_MODEL_KEY` →
recommendation card before sending: Use / Choose another / Always auto-route), temporary chat (server: `ephemeral: true` in
`chatRequestSchema` → stream without persisting; badge "Not stored in history"), message toolbar (desktop hover / mobile
long-press ActionSheet: copy, retry, edit, branch, compare, change model from here, save as prompt, export, rate, more),
expandable message metadata (model, provider, tokens, cost, TTFT, tok/s, reasoning level), deprecated-model warning with
replacement suggestion, humanized provider errors with Retry / Switch model / Details, streaming performance (memoized
blocks, stable markdown, code block Copy/Download/Wrap/Expand, LaTeX via `katex` + `rehype-katex` allowed), inline
**Compare with…** (2–4 models; desktop side-by-side, mobile swipeable panels with sticky tabs; reuse `/api/arena` +
`/api/arena/stream` to run, store as Arena session, "Continue with this model"), empty state with quick actions
(Write / Code / Research / Analyze a file / Compare models), project instructions applied when `activeProjectId` is set
(fetch `/api/projects/:id` — Projects workstream provides it; degrade gracefully if 404).
### B — Models (`components/chat/model-selector.tsx`, `model-config.tsx`, `model-badges.tsx`; `app/app/models/*`; new `app/(marketing)/models/*`, `app/(marketing)/compare/*`; `components/models/*`; `lib/ai/registry/*` (additive); `app/api/models/*` (additive); `components/presets/*`, `app/app/presets/*`; `lib/models/*`)
Model picker redesign (bottom sheet on phones; desktop dialog), fast search with intent parsing ("cheap vision model",
"1M context", "under $1/M", "fastest gemini", "json schema"), sections (Favorites, Recent, Recommended, Fast, Best
reasoning, Cheapest, Largest context, Vision, Coding, Open source, New), compact metadata rows, star favorites, custom labels
(`store.setLabel`), AUTO entry (Smart Router) with mode selector, model badges (NEW/FAST/CHEAP/REASONING/VISION/CODING/LONG
CONTEXT), lifecycle badges (New/Active/Deprecated/Retiring/Unavailable), model profile sheet (all fields; show "—" for
unknown — never invent release dates or cutoffs), `/app/models` catalog (sortable, filters, mobile stacked rows, select →
Compare), public `/models` and `/compare/[slug]` (`gpt-5.5-vs-claude-sonnet-5`) pages with SEO metadata reading the registry
server-side (no auth), capability-aware settings grouped Generation / Reasoning / Output / Tools / Advanced with Reset and
Save preset (presets apply across compatible models).
### C — Arena (`components/arena/*`; `app/app/arena/*`; `lib/arena/*`; `app/api/arena/*`; new `app/share/arena/*`)
Up to 4 models: desktop adaptive 2×2 / side-by-side, phone one panel at a time (`.snap-row`, sticky model tabs, swipe),
live metrics (status, TTFT, tok/s, in/out tokens, cost), votes (Best / Most accurate / Best writing / Best coding / Best value
/ Fastest + custom criteria) stored in `arena_votes`, Arena Winner summary with comparative metrics, Blind Arena (Model A/B/C
until vote; reveal), history, personal scoreboard `/app/arena/scoreboard` (win rate per model, filters Coding/Research/
Writing/Reasoning/Cost efficiency), exports (Markdown/JSON incl. models, parameters, metrics, ratings), public share of an
Arena comparison (`shared_arena_sessions`, `/share/arena/[id]`, revoke).
### D — Projects, prompt library, context library (`components/projects/*`, `components/library/*`, `components/prompts/*`; `app/app/projects/*`, `app/app/library/*`, `app/app/prompts/*`; `lib/projects/*`, `lib/library/*`, `lib/prompts/*`; `app/api/projects/*`, `app/api/library/*`, `app/api/prompts/*`)
Projects CRUD (`/api/projects`, `/api/projects/[id]` returning `{ project, conversations, files, prompts }`), project page
(instructions, preferred models, notes, files, conversations), `activeProjectId` selector, conversations filtered by project
(add `projectId` to `ListFilter`/PATCH in `lib/conversations/service.ts` — small additive change allowed), file library
(`/api/library/files` GET/POST/DELETE with `estimatedTokens`, `/api/library/files/attach` POST `{fileIds}` → `{attachments}`
copying into `message_attachments`), prompt library with `{{variables}}` (parse, defaults, fill-in sheet before insert into
composer via `?prompt=` / `router.push('/app/chat?promptId=…')` and a `usePromptInsert` event on `window`), folders/tags/
favorites, keep existing `prompt_presets`/`model_presets` pages working (migrate the `/app/prompts` page to the new library
while still listing legacy presets). Finish the two stubs listed above.
### E — Usage analytics, providers, custom endpoints (`components/usage/*`; `app/app/usage/*`; `lib/usage/*`; `app/api/usage/*`; `app/app/settings/*`; `components/providers/*`; `lib/providers/*`; `app/api/providers/*`; new `lib/ai/providers/custom/*`, `app/api/endpoints/*`)
Usage dashboard (KPIs: requests, tokens, cost, avg TTFT, tok/s, avg context, error rate; charts: cost/tokens/requests over
time, cost by provider/model, requests by model, latency by model; filters Today/7d/30d/90d/Custom + provider/model/project;
projections "Estimated monthly cost"; savings opportunities computed from real usage_records + pricing; mobile: stacked
KPI tiles, chart cards full-width, tables → rows). Follow `dataviz` skill guidance (palette from tokens, accessible).
Provider management redesign (status Connected/Not connected/Validation failed, last validated, models available, Test
connection with latency, key rotation, remove), extensible provider list. Custom OpenAI-compatible endpoints (Ollama, LM
Studio, vLLM, llama.cpp, MLX…): `custom_endpoints` table exists; implement adapter via `createOpenAICompatAdapter` with
per-endpoint baseURL/headers, discovery (`GET {baseUrl}/models`) or manual models, models keyed `custom/:`
surfaced in `/api/models`, chat routing in `lib/chat/service.ts` resolution (coordinate via a `resolveCustomEndpoint()`
helper you export; document the one-line hook needed in the chat service in your notes if you can't wire it without
editing A's files — the integrator will wire it). Add `"custom"` to `PROVIDER_IDS` only if every switch over providers stays
exhaustive-safe (grep first). Settings pages mobile-first (full-screen sections, bottom sheets).
### F — Marketing, onboarding, auth, PWA polish (`components/marketing/*`; `app/(marketing)/*` except `models`/`compare`; `app/(auth)/*`; `components/app/onboarding.tsx`; `app/app/onboarding/*`; `app/sitemap.ts`; `public/*` (additive))
Homepage redesign: hero "One interface. / Every model." + sub-line (Bring your own keys. Compare models. Control every
parameter. Track every token.), CTAs "Start using PolyLLM" / "Try Arena", animated **real product UI** in the hero (no abstract
art), live model strip (public `GET /api/public/models` summary — create it; marquee with provider marks, context, price,
status), interactive demo switchable Chat/Arena/Models/Usage with simulated streaming and a phone-shaped mobile demo that
mirrors the real mobile UI, features, security page `/security` (Browser → PolyLLM encrypted server layer → Provider; key
lifecycle; AES-256-GCM, Argon2id, HttpOnly cookies, CSP, rate limits, audit logs — technically accurate only, verify in code),
FAQ, footer with **contact@spboucher.ai · Simon-Pierre Boucher · Hosted on MacLustr (www.maclustr.io)**, `/contact` optional.
Onboarding 6 steps (account → providers → first key → validate → favorite models → first prompt; skippable; progress;
swipeable on phone) as a full page `/app/onboarding` + first-run redirect logic in `components/app/onboarding.tsx` (keep the
existing `Onboarding` export working). Auth pages polish (mobile-first). Keep all metadata/OG (already set in `app/layout.tsx`).
### G — Search, command palette, share/export (`components/app/command-palette.tsx`; new `components/search/*`; `app/api/search/*`; `lib/search/*`; `app/share/[id]/*`; `lib/conversations/service.ts` export/share functions (additive); `app/api/conversations/[id]/actions/route.ts` (additive))
Full-text search API with filter syntax `model:claude project:research after:2026-08-01 before: provider: role:` over titles
+ message content (Postgres `ilike`/`to_tsvector` — add a GIN index via a new migration if you use tsvector), grouped results,
snippets with highlights; search UI: desktop inside ⌘K (search mode), phone full-screen sheet (`store.searchOpen`); universal
command palette (New chat, Search, Switch model, Open Arena/Usage/Providers/Project, Upload file, Toggle theme, New temporary
chat (`/app/chat?temporary=1`), Copy conversation URL, keyboard-first). Share: options Entire conversation / Selected messages
(`messageIds`) with privacy warning sheet, revoke, list of active links in Settings → Data; share page redesign (mobile-first,
brand). Export: Markdown, TXT, JSON, PDF (print-optimized route `/share/print/[id]` + `window.print()` is acceptable — label it
"PDF (print)").
## Navigation architecture
- Desktop: sidebar (New chat, Search, Chat/Arena/Models/Prompts/Presets/Library/Usage, Projects, Pinned, Folders, Recent,
account menu). Collapsible ⌘B.
- Phone: bottom nav Chat / Arena / Models / Usage / Account; drawer (hamburger or edge-swipe) for conversations, projects,
search; page headers use a compact 48 px bar with a hamburger on the left (`setSidebarOpen(true)`).
- Every app page must reserve space for the bottom nav (the shell adds `padding-bottom`; pages use `h-full min-h-0 flex-col`
layouts and scroll inside, never the document).
## Integration phase (integrator)
1. Wire cross-area hooks (custom endpoint resolution in chat service, project instructions, prompt insert).
2. `pnpm typecheck && pnpm lint && pnpm test`, `pnpm build`.
3. Single dev server → Playwright QA at 375×812, 390×844, 393×852, 430×932 + desktop 1440 for every screen listed in the brief
(login, signup, onboarding, chat, composer+keyboard, attachments, model switcher, Arena + swipe, drawer, search, projects,
catalog, usage, providers, account, sheets). Fix overflow/clipping/tap targets.
4. Migration on prod via mld hook (`pnpm db:migrate`), deploy `mld stage . polyllm && mld deploy polyllm --node M3U96a`.