From 9800eaac1ff3a88b9478e89d976251569cff9a4a Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Sat, 18 Jul 2026 13:55:26 +0200 Subject: [PATCH 01/10] docs(mobile): add agent instructions and skills for the mobile app --- .agents/skills/mobile-app-structure/SKILL.md | 52 ++++++++++++ .../skills/mobile-motion-patterns/SKILL.md | 44 ++++++++++ .../skills/mobile-shadcn-conventions/SKILL.md | 52 ++++++++++++ .claude/skills/mobile-app-structure | 1 + .claude/skills/mobile-motion-patterns | 1 + .claude/skills/mobile-shadcn-conventions | 1 + web/mobile/AGENTS.md | 84 +++++++++++++++++++ web/mobile/CLAUDE.md | 1 + 8 files changed, 236 insertions(+) create mode 100644 .agents/skills/mobile-app-structure/SKILL.md create mode 100644 .agents/skills/mobile-motion-patterns/SKILL.md create mode 100644 .agents/skills/mobile-shadcn-conventions/SKILL.md create mode 120000 .claude/skills/mobile-app-structure create mode 120000 .claude/skills/mobile-motion-patterns create mode 120000 .claude/skills/mobile-shadcn-conventions create mode 100644 web/mobile/AGENTS.md create mode 120000 web/mobile/CLAUDE.md diff --git a/.agents/skills/mobile-app-structure/SKILL.md b/.agents/skills/mobile-app-structure/SKILL.md new file mode 100644 index 0000000000..fc3a629191 --- /dev/null +++ b/.agents/skills/mobile-app-structure/SKILL.md @@ -0,0 +1,52 @@ +--- +name: mobile-app-structure +description: Feature-folder layout, states/ convention, and data-flow rules for the Agenta mobile app (web/mobile). Use when creating or moving files under web/mobile, deciding where a component lives, adding a new feature or screen, or wiring data into mobile components. +--- + +# Mobile app structure + +The source of truth for how code is organized in `web/mobile`. Load it before +creating any file there. + +## Layout + +```text +web/mobile/ + src/ + pages/ # Pages Router route shells ONLY — no logic, no layout JSX + features/ + / # e.g. sessions/, chat/, auth/, project-drawer/ + .tsx # one component per file, named export = file name + states/ # designed states for this feature + Skeleton.tsx # mirrors the final layout geometry (no shift on swap) + Empty.tsx # designed empty state with a call to action + Error.tsx # error + retry affordance; preserves user input + components/ui/ # shadcn registry components (see mobile-shadcn-conventions) + lib/ # cn util, motion presets, api glue, context resolution + styles/ # globals.css, theme.generated.css (generated) + scripts/ # generate-shadcn-tokens.ts (token bridge) +``` + +## Rules + +- **Pages are thin shells.** A page file resolves route params and renders one + feature screen component. Anything else belongs in `features/`. +- **One component per file.** No secondary exported components; small private + helpers inside a file are fine if they never leave it. +- **Every data-bearing component has designed states.** Before writing the + happy path, create the `states/` siblings (skeleton, empty, error). A screen + is not done if any of its states is a browser default or an unstyled string. +- **Data flow:** components get data via hooks from `@agenta/*` packages + (`@agenta/entities`, `@agenta/shared`, later `@agenta/chat`) or thin fetchers + in `lib/`. NEVER import `@/oss/*`, `@agenta/oss`, `@agenta/ee` — the mobile + app has zero app-layer imports (lint enforces this). +- **No provider fleet.** `_app.tsx` stays minimal; add a provider only when a + concrete feature needs it, scoped as narrowly as possible. + +## Adding a new feature (checklist) + +1. Create `src/features//` with the screen component. +2. Create `states/` siblings for every data-bearing component. +3. Add the route shell in `src/pages/` that renders the screen. +4. Use `useMotionPresets()` for any transitions (see mobile-motion-patterns). +5. `pnpm --filter @agenta/mobile lint && pnpm --filter @agenta/mobile types:check`. diff --git a/.agents/skills/mobile-motion-patterns/SKILL.md b/.agents/skills/mobile-motion-patterns/SKILL.md new file mode 100644 index 0000000000..28d03e5ae2 --- /dev/null +++ b/.agents/skills/mobile-motion-patterns/SKILL.md @@ -0,0 +1,44 @@ +--- +name: mobile-motion-patterns +description: Motion design rules for the Agenta mobile app (web/mobile) — the shared presets in src/lib/motion, when to animate, and reduced-motion requirements. Use when adding any animation or transition under web/mobile, animating navigation, sheets, skeletons, or list/chat surfaces. +--- + +# Mobile motion patterns + +All animation in `web/mobile` uses the `motion` package through the shared +presets module `src/lib/motion/presets.ts`. Components never define their own +durations, easings, or springs. + +## The presets + +Consume via the hook (reduced-motion aware — this is mandatory): + +```tsx +import {useMotionPresets} from "@/lib/motion/presets" + +const {sharedAxisPush, sheetSlideUp, crossfade, reduced} = useMotionPresets() +``` + +- **`sharedAxisPush`** — list → chat navigation (and any parent → child screen + push). Forward uses `custom={1}`, back uses `custom={-1}`; the back + gesture/button reverses the same preset. Wrap sibling screens in + ``. +- **`sheetSlideUp`** — spring-based bottom sheets (project drawer). Pair with a + `crossfade` scrim. +- **`crossfade`** — skeleton → content swaps. Skeleton and content must occupy + identical geometry so the fade causes zero layout shift. + +## Rules + +- **Animate navigation, containment, and state swaps — not decoration.** No + attention-seeking motion, no animating properties that trigger layout + (animate `transform`/`opacity` only). +- **Reduced motion is not optional.** `useMotionPresets()` returns instant + variants when `prefers-reduced-motion` is set; any animation built outside + the presets module must justify itself in review AND handle reduced motion + itself (prefer extending the presets module instead). +- **Message entrance/streaming** (WP3b+): subtle and consistent with the + playground's feel — entrance is a small fade/rise on the preset tokens; text + streaming is never per-character animated. +- New shared patterns go INTO `presets.ts` (one exported preset + doc comment), + not into a component file. diff --git a/.agents/skills/mobile-shadcn-conventions/SKILL.md b/.agents/skills/mobile-shadcn-conventions/SKILL.md new file mode 100644 index 0000000000..23595e47cf --- /dev/null +++ b/.agents/skills/mobile-shadcn-conventions/SKILL.md @@ -0,0 +1,52 @@ +--- +name: mobile-shadcn-conventions +description: How the Agenta mobile app (web/mobile) installs and extends shadcn/ui registry components, themes them via the palette token bridge, and uses Vercel AI Elements. Use when adding UI components under web/mobile, changing theme colors, editing components.json or globals.css, or building chat UI with AI Elements. +--- + +# Mobile shadcn conventions + +`web/mobile` uses shadcn/ui on Tailwind v4 with CSS variables. No antd, ever. + +## Installing registry components + +- Always install via the CLI from `web/mobile/`: + `pnpm dlx shadcn@latest add ` (e.g. `button`, `sheet`, `dialog`, + `command`, `skeleton`, `input`). +- Components land in `src/components/ui/` (aliases in `components.json`). They + are owned code: you may adapt them, but keep diffs minimal and expressed in + semantic tokens so upstream refreshes stay cheap. +- The CLI adds any peer deps (e.g. `@radix-ui/react-slot`) to + `web/mobile/package.json` — commit the manifest and `web/pnpm-lock.yaml` + changes together with the component. +- Never copy component source from the shadcn website by hand; the CLI resolves + the Tailwind v4 variant correctly. + +## Theming — the token bridge + +- shadcn variables (`--background`, `--primary`, ...) are NOT hand-maintained. + They are generated into `src/styles/theme.generated.css` from + `web/oss/src/styles/theme/palette.ts` by `scripts/generate-shadcn-tokens.ts`. +- To change a color: edit `palette.ts` (if the design-system value is wrong) or + the ROLE MAP in the script (if the mapping is wrong), then run + `pnpm --filter @agenta/mobile generate:tokens` and commit the regenerated CSS. +- Never edit `theme.generated.css` directly; never introduce raw hex values in + components — if a needed role is missing, extend the bridge. +- Dark mode is the `.dark` class on `` (`@custom-variant dark` in + `globals.css`), set pre-paint by the `_document.tsx` init script from the + shared `agenta-theme` localStorage key. Both themes must be checked for every + new surface. + +## Extending components + +- Wrap, don't fork: feature-specific variants live in `src/features/*` as thin + wrappers over `components/ui/*` primitives (cva variants where appropriate). +- Use the `cn` util from `@/lib/utils` for all class merging. + +## Vercel AI Elements (chat render layer, WP3b+) + +- AI Elements are shadcn registry components; install them the same way + (`pnpm dlx shadcn@latest add `), landing in + `src/components/ui/` / `src/components/ai-elements/` per the registry config. +- They are the base of the chat skin (Conversation, Message, Response, + Reasoning, Tool, PromptInput); behavior comes from `@agenta/chat` hooks — + never re-implement orchestration inside a rendered component. diff --git a/.claude/skills/mobile-app-structure b/.claude/skills/mobile-app-structure new file mode 120000 index 0000000000..e10ee776ad --- /dev/null +++ b/.claude/skills/mobile-app-structure @@ -0,0 +1 @@ +../../.agents/skills/mobile-app-structure \ No newline at end of file diff --git a/.claude/skills/mobile-motion-patterns b/.claude/skills/mobile-motion-patterns new file mode 120000 index 0000000000..e5bf94d376 --- /dev/null +++ b/.claude/skills/mobile-motion-patterns @@ -0,0 +1 @@ +../../.agents/skills/mobile-motion-patterns \ No newline at end of file diff --git a/.claude/skills/mobile-shadcn-conventions b/.claude/skills/mobile-shadcn-conventions new file mode 120000 index 0000000000..4ed8cec086 --- /dev/null +++ b/.claude/skills/mobile-shadcn-conventions @@ -0,0 +1 @@ +../../.agents/skills/mobile-shadcn-conventions \ No newline at end of file diff --git a/web/mobile/AGENTS.md b/web/mobile/AGENTS.md new file mode 100644 index 0000000000..94566b7690 --- /dev/null +++ b/web/mobile/AGENTS.md @@ -0,0 +1,84 @@ +# Agenta Mobile (`web/mobile`) conventions + +Greenfield mobile web app served at `/m` (Next.js Pages Router, `basePath: "/m"`, +standalone output). This is the always-loaded instruction layer for work under +`web/mobile`. The general frontend conventions in `web/AGENTS.md` (Fern client, +state management, React practices) still apply EXCEPT where this file overrides +them — styling and import rules here are deliberately different. Design doc: +`docs/design/agenta-mobile/design.md`. + +## Hard rules (lint-enforced — see `eslint.config.mjs`) + +- **No antd. Ever.** No `antd`, `@ant-design/*`, `@ant-design/x`, no Lexical. + UI comes from shadcn/ui components in `src/components/ui/` and (for chat, + WP3b+) Vercel AI Elements. Icons come from `lucide-react`. +- **No app-layer imports.** Never import `@/oss/*`, `@agenta/oss`, or + `@agenta/ee`. Data and state come from the `@agenta/*` packages only + (`@agenta/entities`, `@agenta/shared`, `@agenta/chat` when it exists). +- **One component per file**, exported with a name matching the file name. +- **No slab components.** Pages under `src/pages/` are thin route shells only; + each feature is a folder of small single-purpose components. + +## Structure + +```text +web/mobile/src/ + pages/ # thin route shells only (Pages Router) + features// # SessionCard.tsx, SessionSearchBar.tsx, ... one component per file + states/ # Skeleton.tsx, Empty.tsx, Error.tsx — designed sibling states + components/ui/ # shadcn registry components (installed, then owned) + lib/ # motion presets, cn util, api glue — no JSX except tiny helpers + styles/ # globals.css + theme.generated.css (generated, committed) +``` + +## States are designed, not defaulted + +Every screen and every data-bearing component defines loading, empty, error +(and partial, where relevant) states as first-class sibling components in the +feature's `states/` folder. Skeletons mirror the final layout geometry so +content replaces them without shift. Errors carry a retry affordance and never +lose entered state (a failed send never loses the draft). + +## Motion + +All animation uses the `motion` package through the shared presets in +`src/lib/motion/presets.ts`, consumed via `useMotionPresets()` (reduced-motion +aware). Never hardcode durations, easings, or springs in components. Load the +`mobile-motion-patterns` skill before writing any animation code. + +## Styling and theming + +- Tailwind v4, CSS-first config in `src/styles/globals.css`. No + `tailwind.config.*` file exists on purpose. +- Style exclusively with the semantic tokens (`bg-background`, + `text-muted-foreground`, `border-border`, ...). Never hardcode hex/rgb values + in components. +- The color source of truth is `web/oss/src/styles/theme/palette.ts`, bridged + by `scripts/generate-shadcn-tokens.ts` into `src/styles/theme.generated.css` + (committed, never hand-edited). To change a color: edit `palette.ts` or the + role map in the script, then run `pnpm --filter @agenta/mobile generate:tokens`. +- Dark mode is class-based (`.dark` on ``), keyed off the same + `agenta-theme` localStorage value as the desktop app. + +## shadcn registry workflow + +Install or update registry components with `pnpm dlx shadcn@latest add ` +run from `web/mobile/`. Installed components live in `src/components/ui/` and +are owned code — adapt them, but keep diffs from upstream minimal and +token-driven. Load the `mobile-shadcn-conventions` skill for the full workflow. + +## Skills to load when working here + +- `mobile-app-structure` — feature folders, `states/` convention, data-flow rules. +- `mobile-shadcn-conventions` — registry workflow, theming bridge, AI Elements. +- `mobile-motion-patterns` — shared presets, when to animate, reduced motion. + +Also use the plugin skills when relevant: `vercel:nextjs` (Pages Router +specifics), `vercel:shadcn`, `vercel:react-best-practices`. + +## Commands (run from `web/`) + +- Dev: `pnpm dev-mobile` (→ http://localhost:3000/m) +- Build: `pnpm build-mobile` +- Lint / types: `pnpm --filter @agenta/mobile lint` / `pnpm --filter @agenta/mobile types:check` +- Token bridge: `pnpm --filter @agenta/mobile generate:tokens` diff --git a/web/mobile/CLAUDE.md b/web/mobile/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/web/mobile/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 50461c01a4f13b04edda31210a84703b53a56957 Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Sat, 18 Jul 2026 14:48:11 +0200 Subject: [PATCH 02/10] feat(mobile): scaffold @agenta/mobile Next.js app mounted at /m --- web/mobile/.gitignore | 5 + web/mobile/next-env.d.ts | 6 + web/mobile/next.config.ts | 47 ++++ web/mobile/package.json | 46 ++++ web/mobile/postcss.config.mjs | 8 + web/mobile/public/.gitkeep | 0 web/mobile/src/pages/_app.tsx | 21 ++ web/mobile/src/pages/_document.tsx | 25 ++ web/mobile/src/pages/index.tsx | 20 ++ web/mobile/src/styles/globals.css | 1 + web/mobile/tsconfig.json | 24 ++ web/package.json | 3 + web/pnpm-lock.yaml | 381 +++++++++++++++++++++++++++++ web/pnpm-workspace.yaml | 1 + web/turbo.json | 13 + 15 files changed, 601 insertions(+) create mode 100644 web/mobile/.gitignore create mode 100644 web/mobile/next-env.d.ts create mode 100644 web/mobile/next.config.ts create mode 100644 web/mobile/package.json create mode 100644 web/mobile/postcss.config.mjs create mode 100644 web/mobile/public/.gitkeep create mode 100644 web/mobile/src/pages/_app.tsx create mode 100644 web/mobile/src/pages/_document.tsx create mode 100644 web/mobile/src/pages/index.tsx create mode 100644 web/mobile/src/styles/globals.css create mode 100644 web/mobile/tsconfig.json diff --git a/web/mobile/.gitignore b/web/mobile/.gitignore new file mode 100644 index 0000000000..9d49267270 --- /dev/null +++ b/web/mobile/.gitignore @@ -0,0 +1,5 @@ +.next/ +.turbo/ +tsconfig.tsbuildinfo +# runtime config written by web/entrypoint.sh (dev mounts this dir from the host) +public/__env.js diff --git a/web/mobile/next-env.d.ts b/web/mobile/next-env.d.ts new file mode 100644 index 0000000000..254b73c165 --- /dev/null +++ b/web/mobile/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/web/mobile/next.config.ts b/web/mobile/next.config.ts new file mode 100644 index 0000000000..e2b0a59f29 --- /dev/null +++ b/web/mobile/next.config.ts @@ -0,0 +1,47 @@ +import path from "path" + +import type {NextConfig} from "next" + +const isDevelopment = process.env.NODE_ENV === "development" + +const nextConfig: NextConfig = { + // Path mount: Traefik routes PathPrefix(`/m`) here with NO stripprefix — + // the app itself owns the prefix (assets, links, and routes all under /m). + basePath: "/m", + output: "standalone", + reactStrictMode: true, + pageExtensions: ["ts", "tsx"], + productionBrowserSourceMaps: true, + // Workspace root, so standalone output nests as .next/standalone/mobile/ + // (same pattern as web/oss). + outputFileTracingRoot: path.resolve(__dirname, ".."), + // Same policy as web/oss: lint/type gates run as dedicated turbo tasks, + // not inside `next build`. + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, + async headers() { + return [ + { + // `__env.js` is per-deployment RUNTIME config (regenerated on each + // container start by web/entrypoint.sh), not an immutable build + // asset — force it uncacheable. `source` is basePath-relative, + // so this matches /m/__env.js. Mirrors web/oss/next.config.ts. + source: "/__env.js", + headers: [{key: "Cache-Control", value: "no-store, must-revalidate"}], + }, + ] + }, + ...(isDevelopment + ? { + turbopack: { + root: path.resolve(__dirname, ".."), + }, + } + : {}), +} + +export default nextConfig diff --git a/web/mobile/package.json b/web/mobile/package.json new file mode 100644 index 0000000000..16b107e14d --- /dev/null +++ b/web/mobile/package.json @@ -0,0 +1,46 @@ +{ + "name": "@agenta/mobile", + "version": "0.1.0", + "private": true, + "engines": { + "node": "24.x" + }, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build && cp -r public/. .next/standalone/mobile/public && cp -r .next/static .next/standalone/mobile/.next", + "start": "next start", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "format": "prettier --check .", + "format-fix": "prettier --write .", + "types:check": "tsc", + "generate:tokens": "tsx scripts/generate-shadcn-tokens.ts" + }, + "dependencies": { + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.479.0", + "motion": "^12.0.0", + "next": "15.5.18", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.1.0", + "@types/node": "^20.19.20", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-prettier": "^5.5.6", + "prettier": "^3.7.4", + "tailwindcss": "^4.1.0", + "tsx": "^4.22.4", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.61.0" + } +} diff --git a/web/mobile/postcss.config.mjs b/web/mobile/postcss.config.mjs new file mode 100644 index 0000000000..9866e27af5 --- /dev/null +++ b/web/mobile/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config diff --git a/web/mobile/public/.gitkeep b/web/mobile/public/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/mobile/src/pages/_app.tsx b/web/mobile/src/pages/_app.tsx new file mode 100644 index 0000000000..5f9c7c31bd --- /dev/null +++ b/web/mobile/src/pages/_app.tsx @@ -0,0 +1,21 @@ +import type {AppProps} from "next/app" +import Head from "next/head" + +import "@/styles/globals.css" + +// Deliberately minimal: no provider fleet (the desktop _app's ~10 providers +// are the reason this app exists as a separate bundle). Providers are added +// per concern when a feature needs them (auth/session first, in WP2). +export default function App({Component, pageProps}: AppProps) { + return ( + <> + + + + + + ) +} diff --git a/web/mobile/src/pages/_document.tsx b/web/mobile/src/pages/_document.tsx new file mode 100644 index 0000000000..e760362923 --- /dev/null +++ b/web/mobile/src/pages/_document.tsx @@ -0,0 +1,25 @@ +import {Html, Head, Main, NextScript} from "next/document" +import Script from "next/script" + +// Runs synchronously before paint to apply the persisted theme, preventing a +// flash of the wrong theme on load. Same localStorage key as the desktop app +// ("agenta-theme", JSON-encoded by usehooks-ts, default "system") so the +// user's theme follows them between /m and the desktop app. +const themeInitScript = `(function(){try{var r=localStorage.getItem('agenta-theme');var m=r?(r.charAt(0)==='"'?JSON.parse(r):r):'system';var d=m==='dark'||(m==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);if(d){document.documentElement.classList.add('dark');document.documentElement.style.colorScheme='dark';}}catch(e){}})();` + +export default function Document() { + return ( + + +