Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .agents/skills/mobile-app-structure/SKILL.md
Original file line number Diff line number Diff line change
@@ -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/
<feature>/ # e.g. sessions/, chat/, auth/, project-drawer/
<Component>.tsx # one component per file, named export = file name
states/ # designed states for this feature
<X>Skeleton.tsx # mirrors the final layout geometry (no shift on swap)
<X>Empty.tsx # designed empty state with a call to action
<X>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/<feature>/` 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`.
44 changes: 44 additions & 0 deletions .agents/skills/mobile-motion-patterns/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
`<AnimatePresence custom={direction} initial={false}>`.
- **`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.
56 changes: 56 additions & 0 deletions .agents/skills/mobile-shadcn-conventions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
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 <component>` (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.
- Installing a shadcn component that references a NEW token (e.g. `bg-sidebar`,
`chart-*`) requires extending VARS in `scripts/generate-shadcn-tokens.ts` +
the `@theme inline` map in `globals.css` first — Tailwind v4 silently
generates nothing for unmapped tokens.

## 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 `<html>` (`@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 <ai-elements registry item>`), 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.
1 change: 1 addition & 0 deletions .claude/skills/mobile-app-structure
1 change: 1 addition & 0 deletions .claude/skills/mobile-motion-patterns
1 change: 1 addition & 0 deletions .claude/skills/mobile-shadcn-conventions
37 changes: 37 additions & 0 deletions hosting/docker-compose/ee/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,41 @@ services:
# === LIFECYCLE ============================================ #
restart: always

web-mobile:
# === ACTIVATION =========================================== #
profiles:
- with-web
# === IMAGE ================================================ #
image: agenta-ee-dev-web:latest
# === EXECUTION ============================================ #
command: sh -c "pnpm dev-mobile"
# === STORAGE ============================================== #
volumes:
- ../../../web/mobile/src:/app/mobile/src
- ../../../web/mobile/public:/app/mobile/public
- nextjs-mobile-cache:/app/mobile/.next/cache
- turbo-mobile-cache:/app/.turbo
# === CONFIGURATION ======================================== #
env_file:
- ${ENV_FILE:-./.env.ee.dev}
environment:
DOCKER_NETWORK_MODE: ${DOCKER_NETWORK_MODE:-bridge}
WATCHPACK_POLLING: "true"
# === NETWORK ============================================== #
networks:
- agenta-network
# === LABELS =============================================== #
# Path(`/m`) || PathPrefix(`/m/`), not PathPrefix(`/m`): the bare prefix is
# greedy and would also swallow /mfoo from the web catch-all PathPrefix(`/`).
# Still auto-wins over that catch-all by rule length; no stripprefix — the app
# is built with basePath /m.
labels:
- "traefik.http.routers.agenta-web-mobile.rule=Path(`/m`) || PathPrefix(`/m/`)"
- "traefik.http.routers.agenta-web-mobile.entrypoints=web"
- "traefik.http.services.agenta-web-mobile.loadbalancer.server.port=3000"
# === LIFECYCLE ============================================ #
restart: always
Comment thread
coderabbitai[bot] marked this conversation as resolved.

api:
# === IMAGE ================================================ #
image: agenta-ee-dev-api:latest
Expand Down Expand Up @@ -772,3 +807,5 @@ volumes:
nextjs-ee-cache:
nextjs-oss-cache:
turbo-ee-cache:
nextjs-mobile-cache:
turbo-mobile-cache:
37 changes: 37 additions & 0 deletions hosting/docker-compose/oss/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,41 @@ services:
# === LIFECYCLE ============================================ #
restart: always

web-mobile:
# === ACTIVATION =========================================== #
profiles:
- with-web
# === IMAGE ================================================ #
image: agenta-oss-dev-web:latest
# === EXECUTION ============================================ #
command: sh -c "pnpm dev-mobile"
# === STORAGE ============================================== #
volumes:
- ../../../web/mobile/src:/app/mobile/src
- ../../../web/mobile/public:/app/mobile/public
- nextjs-mobile-cache:/app/mobile/.next/cache
- turbo-mobile-cache:/app/.turbo
# === CONFIGURATION ======================================== #
env_file:
- ${ENV_FILE:-./.env.oss.dev}
environment:
DOCKER_NETWORK_MODE: ${DOCKER_NETWORK_MODE:-bridge}
WATCHPACK_POLLING: "true"
# === NETWORK ============================================== #
networks:
- agenta-network
# === LABELS =============================================== #
# Path(`/m`) || PathPrefix(`/m/`), not PathPrefix(`/m`): the bare prefix is
# greedy and would also swallow /mfoo from the web catch-all PathPrefix(`/`).
# Still auto-wins over that catch-all by rule length; no stripprefix — the app
# is built with basePath /m.
labels:
- "traefik.http.routers.agenta-web-mobile.rule=Path(`/m`) || PathPrefix(`/m/`)"
- "traefik.http.routers.agenta-web-mobile.entrypoints=web"
- "traefik.http.services.agenta-web-mobile.loadbalancer.server.port=3000"
# === LIFECYCLE ============================================ #
restart: always

api:
# === IMAGE ================================================ #
image: agenta-oss-dev-api:latest
Expand Down Expand Up @@ -747,3 +782,5 @@ volumes:
seaweed-data:
nextjs-oss-cache:
turbo-oss-cache:
nextjs-mobile-cache:
turbo-mobile-cache:
6 changes: 5 additions & 1 deletion web/ee/docker/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ RUN PNPM_VERSION=$(node -p "require('./package.json').packageManager.split('@')[
# Copy app package manifests
COPY ee/package.json ./ee/yarn.lock* ./ee/package-lock.json* ./ee/pnpm-lock.yaml* ./ee/.npmrc* ./ee/
COPY oss/package.json ./oss/yarn.lock* ./oss/package-lock.json* ./oss/pnpm-lock.yaml* ./oss/.npmrc* ./oss/
COPY mobile/package.json ./mobile/

# Copy workspace package manifests (required for workspace:* resolution)
COPY packages/agenta-shared/package.json ./packages/agenta-shared/
Expand Down Expand Up @@ -57,7 +58,7 @@ COPY ./entrypoint.sh /app/entrypoint.sh

RUN groupadd --gid 10001 agenta && \
useradd --uid 10001 --gid 10001 --shell /bin/false --create-home agenta && \
mkdir -p /app/.turbo /app/ee/.next/cache /app/oss/.next/cache && \
mkdir -p /app/.turbo /app/ee/.next/cache /app/oss/.next/cache /app/mobile/.next/cache && \
chown -R agenta:agenta /app

USER 10001
Expand Down Expand Up @@ -107,6 +108,9 @@ COPY --chown=agenta:agenta oss/next.config.ts ./oss/next.config.ts
COPY --chown=agenta:agenta ee/tailwind.config.ts ./ee/tailwind.config.ts
COPY --chown=agenta:agenta oss/tailwind.config.ts ./oss/tailwind.config.ts

COPY --chown=agenta:agenta mobile/src ./mobile/src
COPY --chown=agenta:agenta mobile/public ./mobile/public
COPY --chown=agenta:agenta mobile/tsconfig.json mobile/next-env.d.ts mobile/next.config.ts mobile/postcss.config.mjs mobile/components.json ./mobile/

ENTRYPOINT ["./entrypoint.sh"]
EXPOSE 3000
9 changes: 9 additions & 0 deletions web/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,13 @@ EOF

cat "${ENTRYPOINT_DIR}/${AGENTA_LICENSE}/public/__env.js" >&2

# Mirror the runtime config into the mobile app's public dir (served at
# /m/__env.js — the mobile app is built with basePath /m). The mobile app is
# edition-agnostic: one file, same content for oss and ee. Guarded so images
# without the mobile app (current gh images) are unaffected.
if [ -d "${ENTRYPOINT_DIR}/mobile" ]; then
mkdir -p "${ENTRYPOINT_DIR}/mobile/public"
cp "${ENTRYPOINT_DIR}/${AGENTA_LICENSE}/public/__env.js" "${ENTRYPOINT_DIR}/mobile/public/__env.js"
fi

exec "$@"
5 changes: 5 additions & 0 deletions web/mobile/.gitignore
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions web/mobile/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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/<feature>/ # 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 `<html>`), 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 <name>`
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`
1 change: 1 addition & 0 deletions web/mobile/CLAUDE.md
Loading
Loading