Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ directories until the move is complete.

| Directory | Status |
|---|---|
| `apps/web/` | Active migration target — Vite + React Router v7 SPA for the assistant web app. Code is being incrementally migrated from a separate repo. See [`apps/web/README.md`](apps/web/README.md) for local dev setup, [`apps/web/CONVENTIONS.md`](apps/web/CONVENTIONS.md) for architecture and state management patterns, and [`apps/web/STYLE_GUIDE.md`](apps/web/STYLE_GUIDE.md) for coding style. |
| `apps/web/` | Active migration target — Vite + React Router v7 SPA for the assistant web app. Code is being incrementally migrated from a separate repo. See [`apps/web/README.md`](apps/web/README.md) for local dev setup, [`apps/web/docs/CONVENTIONS.md`](apps/web/docs/CONVENTIONS.md) and [`apps/web/docs/STATE_MANAGEMENT.md`](apps/web/docs/STATE_MANAGEMENT.md) for architecture and state, and [`apps/web/docs/STYLE_GUIDE.md`](apps/web/docs/STYLE_GUIDE.md) for coding style. |
| `apps/chrome-extension/` | Move in progress from [`clients/chrome-extension/`](https://github.com/vellum-ai/vellum-assistant/tree/main/clients/chrome-extension). |

## Submitting a pull request
Expand Down
2 changes: 1 addition & 1 deletion apps/ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Apple's reference for the toolbar controls:

> **Web-side conventions for iOS code paths**: any change to the web app
> that might run inside this WKWebView shell needs to follow the patterns
> in [`apps/web/CAPACITOR.md`](../web/CAPACITOR.md) — Capacitor plugin
> in [`apps/web/docs/CAPACITOR.md`](../web/docs/CAPACITOR.md) — Capacitor plugin
> lazy imports, native auth, deep links, autogrowing textareas,
> streaming watchdogs, OS permission UI, etc.

Expand Down
28 changes: 15 additions & 13 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# Web App — Agent Instructions

Applies to all code under `apps/web/`. Subordinate to [`apps/AGENTS.md`](../AGENTS.md) and root [`AGENTS.md`](../../AGENTS.md).
Applies to all code under `apps/web/`. For broader patterns see [`apps/AGENTS.md`](../AGENTS.md) and root [`AGENTS.md`](../../AGENTS.md).

## Conventions and style

Read these before making changes:

- **[`CONVENTIONS.md`](./CONVENTIONS.md)** — Architecture, code organization, state management, component patterns, framework strategy, data fetching, testing.
- **[`STYLE_GUIDE.md`](./STYLE_GUIDE.md)** — Naming, imports, TypeScript, component authoring, formatting.
- **[`CAPACITOR.md`](./CAPACITOR.md)** — Capacitor / iOS patterns: lazy plugin imports, native auth, deep links, autogrowing textareas, streaming watchdogs, OS permission UI, capability detection, keyboard-only affordances. Mandatory reading if any code path you're touching might run inside the iOS WKWebView shell.
- **[`docs/CONVENTIONS.md`](./docs/CONVENTIONS.md)** — Architecture, code organization, component patterns, framework strategy, data fetching, testing.
- **[`docs/STATE_MANAGEMENT.md`](./docs/STATE_MANAGEMENT.md)** — Zustand stores, atomic selectors, TanStack Query, the no-`useReducer` rule.
- **[`docs/STYLE_GUIDE.md`](./docs/STYLE_GUIDE.md)** — Naming, imports, TypeScript, component authoring, formatting.
- **[`docs/CAPACITOR.md`](./docs/CAPACITOR.md)** — Capacitor / iOS patterns: lazy plugin imports, native auth, deep links, autogrowing textareas, streaming watchdogs, OS permission UI, capability detection, keyboard-only affordances. Mandatory reading if any code path you're touching might run inside the iOS WKWebView shell.
Comment on lines +9 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 CONTRIBUTING.md and apps/ios/README.md still reference old file paths

This PR moved CONVENTIONS.md, STYLE_GUIDE.md, and CAPACITOR.md from apps/web/ to apps/web/docs/, but two files outside apps/web/ that link to the old paths were not updated:

  • CONTRIBUTING.md:95 links to apps/web/CONVENTIONS.md and apps/web/STYLE_GUIDE.md — both now 404
  • apps/ios/README.md:98 links to ../web/CAPACITOR.md — should be ../web/docs/CAPACITOR.md

This violates the root AGENTS.md rule: "When introducing, removing, or significantly modifying a service/module/data flow, update AGENTS.md and impacted domain docs."

Prompt for agents
Two files outside apps/web/ reference the old pre-move paths and need updating:

1. CONTRIBUTING.md line 95: Change apps/web/CONVENTIONS.md to apps/web/docs/CONVENTIONS.md, and apps/web/STYLE_GUIDE.md to apps/web/docs/STYLE_GUIDE.md.

2. apps/ios/README.md line 98: Change ../web/CAPACITOR.md to ../web/docs/CAPACITOR.md.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


When a topic in `docs/CONVENTIONS.md` grows past ~100 lines and has a
coherent boundary, extract it into a `docs/TOPIC.md` sibling with a
short pointer back from `CONVENTIONS.md`. Matches the repo's existing
pattern (`assistant/docs/`, `docs/` at the repo root).

## Stack

- **Build**: [Vite](https://vite.dev/) + [React 19](https://react.dev/blog/2024/12/05/react-19)
- **Routing**: [React Router v7](https://reactrouter.com/) — [data mode](https://reactrouter.com/start/modes) (`createBrowserRouter`), NOT framework mode
- **Client state**: [Zustand](https://zustand.docs.pmnd.rs/) — all shared state uses Zustand stores (see [CONVENTIONS.md — State management](./CONVENTIONS.md#state-management))
- **Client state**: [Zustand](https://zustand.docs.pmnd.rs/) — all shared state uses Zustand stores (see [`docs/STATE_MANAGEMENT.md`](./docs/STATE_MANAGEMENT.md))
- **Server state**: [TanStack Query](https://tanstack.com/query/latest) with [HeyAPI plugin](https://heyapi.dev/openapi-ts/plugins/tanstack-query)
- **Styling**: [Tailwind CSS v4](https://tailwindcss.com/) via `@tailwindcss/vite`
- **Design system**: `@vellum/design-library` at [`packages/design-library/`](../../packages/design-library/)
Expand All @@ -25,8 +31,8 @@ Read these before making changes:
- Route config: `src/routes.tsx`
- Route constants: `src/utils/routes.ts` — all paths are absolute browser paths
- No `basename` on the router — `/account/*` and `/assistant/*` are explicit top-level branches
- Routes must match the platform repo exactly during migration (no URL changes)
- **Route protection**: uses React Router v7 [middleware](https://reactrouter.com/how-to/middleware) (`v8_middleware` future flag), not layout gate components or `useEffect` redirects. Auth is always required — the middleware redirects unauthenticated users to `/account/login`. See [CONVENTIONS.md — Route protection via middleware](./CONVENTIONS.md#route-protection-via-middleware).
- URL paths are part of the contract — bookmarks and deep links depend on them. Don't rename URL patterns without a deprecation period.
- **Route protection**: uses React Router v7 [middleware](https://reactrouter.com/how-to/middleware) (`v8_middleware` future flag), not layout gate components or `useEffect` redirects. Auth is always required — the middleware redirects unauthenticated users to `/account/login`. See [`docs/CONVENTIONS.md` — Route protection via middleware](./docs/CONVENTIONS.md#route-protection-via-middleware).
- **Assistant lifecycle**: owned by `ChatLayout`, passed to child routes via [outlet context](https://reactrouter.com/start/framework/outlet). Child routes consume the resolved `assistantId` via `useAssistantContext()` — never hardcode or independently resolve it.

## Commands
Expand All @@ -42,10 +48,6 @@ cd apps/web && bun test src/path/to/file.test.ts # Run specific tests
cd apps/web && bun run test:ci # Run all tests (isolated, CI)
```

## Migration status

This app is being migrated from [`vellum-assistant-platform/web/`](https://github.com/vellum-ai/vellum-assistant-platform). During migration:
## Scope

- **Faithful copy, not simplification.** Port real implementations, not stubs. All Capacitor/native code paths must be preserved.
- **Convention compliance on arrival.** Apply this repo's naming (kebab-case), import conventions (`.js` extensions, `@/` aliases), and directory structure as code is ported.
- **No marketing or admin pages.** Only the assistant web app and auth/identity pages are migrating.
This package contains only the assistant web app and authentication / identity pages. Marketing pages and admin/internal surfaces are out of scope.
11 changes: 6 additions & 5 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Vellum assistant web app (chat, settings, library, docs).
`<RouterProvider>`).
- [Zustand](https://zustand.docs.pmnd.rs/) for shared client state
(messages, streaming, interactions, conversations). See
[`CONVENTIONS.md`](./CONVENTIONS.md#state-management) for store patterns.
[`docs/STATE_MANAGEMENT.md`](./docs/STATE_MANAGEMENT.md) for store patterns.
- [TanStack React Query](https://tanstack.com/query/latest) for server
state (API calls, caching, mutations).
- [HeyAPI](https://heyapi.dev/) for OpenAPI client generation with
Expand Down Expand Up @@ -104,11 +104,12 @@ deterministic results are required.

## Architecture

See [`CONVENTIONS.md`](./CONVENTIONS.md) for code organization
(domain-based architecture), state management patterns (Zustand +
React Query), component conventions, and framework strategy.
See [`docs/CONVENTIONS.md`](./docs/CONVENTIONS.md) for code organization
(domain-based architecture), component conventions, and framework strategy.
See [`docs/STATE_MANAGEMENT.md`](./docs/STATE_MANAGEMENT.md) for state
patterns (Zustand + TanStack Query).

See [`STYLE_GUIDE.md`](./STYLE_GUIDE.md) for naming, imports,
See [`docs/STYLE_GUIDE.md`](./docs/STYLE_GUIDE.md) for naming, imports,
TypeScript rules, and formatting.

## Directory structure
Expand Down
19 changes: 10 additions & 9 deletions apps/web/CAPACITOR.md → apps/web/docs/CAPACITOR.md
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Broken relative links in CAPACITOR.md after move to docs/ subdirectory

When CAPACITOR.md was moved from apps/web/ to apps/web/docs/, many relative links to source files (./src/...) and parent directories (../../apps/ios/) were not adjusted. The ./src/ prefix now resolves to the non-existent apps/web/docs/src/ instead of apps/web/src/, and ../../apps/ios/ resolves to non-existent apps/apps/ios/.

All broken links
  • Line 7: (../../apps/ios/)apps/apps/ios/ (should be ../../../apps/ios/)
  • Line 42: (./src/runtime/native-auth.ts) → non-existent (should be ../src/runtime/native-auth.ts)
  • Line 42: (../../apps/ios/App/App/NativeAuthPlugin.swift) → non-existent (should be ../../../apps/ios/App/App/NativeAuthPlugin.swift)
  • Line 44: (./src/components/native-splash.tsx) → non-existent (should be ../src/components/native-splash.tsx)
  • Line 48: (./src/runtime/native-auth.ts) → non-existent (should be ../src/runtime/native-auth.ts)
  • Line 67: (./src/utils/pointer.ts) → non-existent (should be ../src/utils/pointer.ts)
  • Line 75: (./src/runtime/native-deep-link.ts) → non-existent (should be ../src/runtime/native-deep-link.ts)

(Refers to line 7)

Prompt for agents
CAPACITOR.md was moved from apps/web/ to apps/web/docs/ but its relative links were not updated.

Replace all ./src/ prefixes with ../src/ (7 occurrences on lines 42, 44, 48, 67, 75).
Replace ../../apps/ with ../../../apps/ (lines 7, 42).

Specifically:
- Line 7: (../../apps/ios/) -> (../../../apps/ios/)
- Line 42: (./src/runtime/native-auth.ts) -> (../src/runtime/native-auth.ts)
- Line 42: (../../apps/ios/App/App/NativeAuthPlugin.swift) -> (../../../apps/ios/App/App/NativeAuthPlugin.swift)
- Line 44: (./src/components/native-splash.tsx) -> (../src/components/native-splash.tsx)
- Line 48: (./src/runtime/native-auth.ts) -> (../src/runtime/native-auth.ts)
- Line 67: (./src/utils/pointer.ts) -> (../src/utils/pointer.ts)
- Line 75: (./src/runtime/native-deep-link.ts) -> (../src/runtime/native-deep-link.ts)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

The web app ships as both a browser SPA and the JS layer of a [Capacitor](https://capacitorjs.com/) iOS shell that loads it in a `WKWebView`. The patterns below are mandatory for any code path that might run inside Capacitor iOS — most of them address real iOS-specific failure modes that desktop browsers silently tolerate.

If you're touching anything in `apps/web/src/runtime/`, anything that calls a `@capacitor/*` plugin, anything that streams from the daemon, anything that auto-resizes based on content, or anything that gates a browser API that triggers an OS permission alert — start here.
> **Read this only if your change touches iOS code paths.** For browser-only contributions you can skip this document. Building the iOS app itself additionally requires macOS and Xcode; the native shell lives in [`apps/ios/`](../../../apps/ios/).

The native iOS shell that consumes these patterns lives at [`apps/ios/`](../../apps/ios/).
If you're touching anything in `apps/web/src/runtime/`, anything that calls a `@capacitor/*` plugin, anything that streams from the daemon, anything that auto-resizes based on content, or anything that gates a browser API that triggers an OS permission alert — start here.

---

Expand Down Expand Up @@ -39,13 +39,13 @@ References:

## Native auth on iOS

Native auth uses [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) (Safari sheet) via a `NativeAuth` Capacitor plugin — see [`src/runtime/native-auth.ts`](./src/runtime/native-auth.ts) and the Swift side at [`apps/ios/App/App/NativeAuthPlugin.swift`](../../apps/ios/App/App/NativeAuthPlugin.swift).
Native auth uses [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) (Safari sheet) via a `NativeAuth` Capacitor plugin — see [`src/runtime/native-auth.ts`](../src/runtime/native-auth.ts) and the Swift side at [`apps/ios/App/App/NativeAuthPlugin.swift`](../../../apps/ios/App/App/NativeAuthPlugin.swift).

- **Protected (app) routes**: route protection middleware (see [`CONVENTIONS.md` § Route protection via middleware](./CONVENTIONS.md#route-protection-via-middleware)) redirects unauthenticated users to `/account/login?returnTo=…`. Individual pages should **not** render inline sign-in gates. Return `null` when `!isLoggedIn` and let the middleware handle the redirect. The branded login page (`/account/login`) renders a native login form (inside [`NativeSplash`](./src/components/native-splash.tsx)) on Capacitor iOS and a web login form on web.
- **Protected (app) routes**: route protection middleware (see [`CONVENTIONS.md` § Route protection via middleware](./CONVENTIONS.md#route-protection-via-middleware)) redirects unauthenticated users to `/account/login?returnTo=…`. Individual pages should **not** render inline sign-in gates. Return `null` when `!isLoggedIn` and let the middleware handle the redirect. The branded login page (`/account/login`) renders a native login form (inside [`NativeSplash`](../src/components/native-splash.tsx)) on Capacitor iOS and a web login form on web.
- **iOS login — no `providerHint`**: the iOS login form must use a single "Sign in" button with **no `providerHint`**. Do NOT add individual provider buttons or pass `providerHint` from the iOS login screen — see [Apple App Store Review Guideline 4](https://developer.apple.com/app-store/review/guidelines/#design) and [Guideline 4.8 — Sign in with Apple](https://developer.apple.com/app-store/review/guidelines/#login-services). The `providerHint` / `loginHint` parameters remain in the helper API for web and other use cases but must not be used from the iOS login entry point.
- **Pre-fill identity-derived inputs from the auth claim**: when the platform / IdP returns identity claims on signup (Apple SIWA `given_name`/`family_name`, Google `given_name`/`family_name`, etc.), pre-fill any user-facing input that asks for that identity (e.g. "Your name") from the claim instead of forcing the user to retype it — [Apple Guideline 4](https://developer.apple.com/app-store/review/guidelines/#design) and [Apple HIG: Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple) treat asking again as a violation. The field stays editable so users can pick a preferred nickname.
- **Sign-in actions outside the app shell**: wrap sign-in links in a shared component that renders a native `startAuthFlow()` button on Capacitor iOS and a router `<Link>` on web — never a plain `<a href="/account/login">`, which on iOS would navigate the WKWebView away from the running SPA.
- **Platform detection in JSX**: use a `useIsNativePlatform()` hook (which returns `false` during the first paint and settles to the real value on mount) — not the bare `isNativePlatform()` function — to avoid render flicker and hydration mismatches in any SSR/prerender path. If the hook doesn't exist yet at the call site, add it next to [`src/runtime/native-auth.ts`](./src/runtime/native-auth.ts).
- **Platform detection in JSX**: use a `useIsNativePlatform()` hook (which returns `false` during the first paint and settles to the real value on mount) — not the bare `isNativePlatform()` function — to avoid render flicker and hydration mismatches in any SSR/prerender path. If the hook doesn't exist yet at the call site, add it next to [`src/runtime/native-auth.ts`](../src/runtime/native-auth.ts).

### Platform short-circuits in capability detection

Expand All @@ -64,15 +64,15 @@ Apple's [HIG — Requesting permission](https://developer.apple.com/design/human

### Keyboard-only affordances on touch devices

When the *only* way to act on a UI element is a hardware-keyboard gesture (e.g. `Tab` to accept an inline suggestion, `Cmd+Enter` to submit), gate its rendering on `!isPointerCoarse()` from [`@/utils/pointer`](./src/utils/pointer.ts). Touch soft keyboards on iOS and Android do not expose `Tab` or most modifier-key combinations, so the affordance is non-actionable on coarse-pointer devices and may also overflow narrow viewports if its layout depends on a paired keypress. To support touch as well, add a tap-equivalent (button, gesture) instead of suppressing.
When the *only* way to act on a UI element is a hardware-keyboard gesture (e.g. `Tab` to accept an inline suggestion, `Cmd+Enter` to submit), gate its rendering on `!isPointerCoarse()` from [`@/utils/pointer`](../src/utils/pointer.ts). Touch soft keyboards on iOS and Android do not expose `Tab` or most modifier-key combinations, so the affordance is non-actionable on coarse-pointer devices and may also overflow narrow viewports if its layout depends on a paired keypress. To support touch as well, add a tap-equivalent (button, gesture) instead of suppressing.

Reference: [MDN: `(pointer)` media feature](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer).

---

## Deep links (Capacitor `appUrlOpen`)

Native OAuth completion auto-dismisses `SFSafariViewController` by redirecting to a registered custom URL scheme (`vellum-assistant://`, `-dev`, `-staging`) and routing the URL via the `@capacitor/app` plugin's `appUrlOpen` listener. The router is mounted globally for the app routes; pure utilities and the typed `WindowEventMap` augmentation live in [`src/runtime/native-deep-link.ts`](./src/runtime/native-deep-link.ts).
Native OAuth completion auto-dismisses `SFSafariViewController` by redirecting to a registered custom URL scheme (`vellum-assistant://`, `-dev`, `-staging`) and routing the URL via the `@capacitor/app` plugin's `appUrlOpen` listener. The router is mounted globally for the app routes; pure utilities and the typed `WindowEventMap` augmentation live in [`src/runtime/native-deep-link.ts`](../src/runtime/native-deep-link.ts).

- **Build deep links via `buildOAuthCompleteDeepLink()`.** Don't hand-construct URLs — the helper picks the right scheme per host (`getNativeUrlSchemeForHost`) and encodes the payload consistently.
- **Parse via `parseOAuthCompleteDeepLink()`.** It exact-matches the scheme against the apex allow-list, rejects look-alikes (e.g. `vellum-assistant-evil://`), requires the `oauth-complete` host, and enforces a non-empty `requestId`. Adding a new scheme means adding it to the allow-list — do not loosen the matcher to a `startsWith` check.
Expand Down Expand Up @@ -117,6 +117,7 @@ References:

## See also

- [`CONVENTIONS.md`](./CONVENTIONS.md) — architecture, code organization, state management, component patterns.
- [`CONVENTIONS.md`](./CONVENTIONS.md) — architecture, code organization, component patterns.
- [`STATE_MANAGEMENT.md`](./STATE_MANAGEMENT.md) — Zustand stores, atomic selectors, TanStack Query.
- [`STYLE_GUIDE.md`](./STYLE_GUIDE.md) — naming, imports, TypeScript, component authoring.
- [`apps/ios/README.md`](../../apps/ios/README.md) — Capacitor iOS shell setup, Xcode targets, release pipeline.
- [`apps/ios/README.md`](../../../apps/ios/README.md) — Capacitor iOS shell setup, Xcode targets, release pipeline.
Loading