{"owner":"sanity-io","repo":"sanity","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# AGENTS.md - AI Agent Guidelines for Sanity Monorepo\n\nThis document helps AI agents work successfully with the Sanity monorepo.\n\n> **Self-Improvement:** If you discover undocumented requirements, commands, or workflows during your work (e.g., a reviewer asks you to run something not covered here), update this file on the same PR. Keep this guide accurate and helpful for future agents.\n\n## Prerequisites\n\n- **Node.js**: v24 or latest LTS\n- **Package Manager**: pnpm v10+ (exact version managed via `packageManager` field in package.json)\n\n## Quick Reference\n\n```bash\n# Install dependencies (pnpm ONLY - enforced)\npnpm install\n\n# Build all packages (required before testing)\npnpm build\n\n# Run dev studio (requires auth, see below)\npnpm dev\n\n# Format code (MUST pass CI)\npnpm chore:format:fix\n\n# Fix all lint issues (MUST pass CI) — includes TypeScript type checking via oxlint\npnpm lint:fix\n\n# Run tests\npnpm test\n\n# Update snapshots if tests fail due to expected changes\npnpm test -- -u\n\n# Lint + type check (oxlint typeAware + typeCheck; no separate tsc step)\npnpm check:oxlint\n```\n\n## CI Checks - What Must Pass\n\nThese checks run on every PR and **must pass**:\n\n| Check            | Command               | Notes                                                                                                                                                            |\n| ---------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Format**       | `pnpm check:format`   | Uses oxfmt. Fix with `pnpm chore:format:fix`                                                                                                                     |\n| **Oxlint**       | `pnpm check:oxlint`   | Rust linter with type-aware rules and TypeScript type checking via tsgolint (`options.typeCheck`). Fix with `pnpm chore:oxlint:fix`                              |\n| **Unit Tests**   | `pnpm test`           | Vitest, sharded in CI                                                                                                                                            |\n| **Export Tests** | `pnpm test:exports`   | Ensures ESM/CJS/DTS work                                                                                                                                         |\n| **Dep Check**    | `pnpm depcheck`       | Finds unused/missing deps                                                                                                                                        |\n| **Zizmor**       | `pnpm lint:workflows` | Audits `.github/workflows/` for security issues. Fails CI on high-severity findings. Local run needs [`zizmor`](https://docs.zizmor.sh/installation/) on `PATH`. |\n| **PR Title**     | Conventional commits  | e.g., `feat(scope): description`                                                                                                                                 |\n\n### Before Committing\n\nRun these commands to avoid CI failures:\n\n```bash\n# Fix all formatting and lint issues\npnpm lint:fix\n\n# Verify tests pass (build first if needed)\npnpm build && pnpm test\n```\n\nIf tests fail due to **expected snapshot changes**, update them:\n\n```bash\npnpm test -- -u\n```\n\nSnapshot files are located in `__snapshots__` directories alongside test files.\n\n## Project Structure\n\n```\nsanity/\n├── packages/\n│   ├── sanity/           # Main Sanity studio package\n│   ├── @sanity/          # Scoped packages (cli, types, schema, etc.)\n│   └── @repo/            # Internal tooling (test-config, tsconfig, etc.)\n├── dev/                  # Development studios for testing\n│   ├── test-studio/      # Primary dev studio (pnpm dev runs this)\n│   └── preview-iframe/   # Presentation preview iframe (vanilla Vite, port 3334)\n├── e2e/                  # End-to-end Playwright tests\n├── perf/                 # Performance testing\n└── examples/             # Example studios\n```\n\n### Key Packages\n\n- **`packages/sanity`** - Core studio package with all UI components\n- **`packages/@sanity/types`** - TypeScript type definitions\n- **`packages/@sanity/schema`** - Schema compilation\n- **`packages/@sanity/mutator`** - Document mutation logic\n\n## Build System\n\n- **Package Manager**: pnpm (version 10.x, enforced via `preinstall`)\n- **Build Orchestration**: Turbo (caches builds)\n- **Versioning**: Lerna-lite with conventional commits\n\n### Build Commands\n\n```bash\npnpm build              # Build all packages\npnpm watch              # Watch mode for development\n```\n\n### Running the Dev Studio\n\n```bash\npnpm dev                # Starts dev studio at http://localhost:3333\n```\n\n**Note:** The dev studio requires Sanity user authentication in the browser. It's a Vite application that communicates with Sanity API endpoints, so you'll need to log in with a Sanity account when you access `http://localhost:3333` to use the studio.\n\n## Local Development\n\nThis section clarifies what requires authentication and what doesn't—critical for AI agents to avoid getting stuck on auth flows.\n\n### Running Tests (No Auth Required)\n\nUnit tests run in jsdom with mocks and **do not require any authentication**:\n\n```bash\n# Build first (required), then run all tests\npnpm build && pnpm test\n\n# Run a single test file (IMPORTANT: use vitest directly with --project to avoid running all tests)\npnpm vitest run --project=sanity packages/sanity/src/core/hooks/useClient.test.ts\n\n# Run a single test file with verbose output\npnpm vitest run --project=sanity --reporter=verbose packages/sanity/src/core/hooks/useClient.test.ts\n\n# Watch mode for iterative development\npnpm test -- --watch\n\n# Run tests for a specific package\npnpm test -- --project=sanity\n```\n\n**Important:** Do NOT use `pnpm test -- path/to/file.test.ts` for running a single file — it runs all tests across all projects. Use `pnpm vitest run --project=<project> <path>` instead.\n\nComponents that need auth context use `createMockAuthStore` in tests, so no real authentication is needed. This is the recommended way to verify most code changes.\n\n### Running the Dev Studio (Auth Required)\n\n```bash\npnpm dev  # Starts test-studio at http://localhost:3333 and preview-iframe at http://localhost:3334\n```\n\n- **Requires browser authentication** on first visit—you'll be prompted to log in with a Sanity account\n- Connects to a real Sanity project (configured in `dev/test-studio/sanity.config.ts`)\n- Uses staging API by default (`api.sanity.work`)\n- Session persists in browser, so subsequent visits won't require re-authentication\n- `pnpm dev` / `pnpm dev:test-studio` also starts `dev/preview-iframe` (vanilla Vite on port 3334) so Presentation can load its cross-origin iframe. Studio-only: `pnpm dev:test-studio:studio`. Preview-only: `pnpm dev:preview-iframe`.\n- Deployed preview iframe: Sanity Sandbox Vercel project `test-studio-preview-iframe` (`https://test-studio-preview-iframe.sanity.dev`)\n\nUse the dev studio when you need to:\n\n- Visually verify UI changes\n- Test real document editing workflows\n- Debug issues that only appear with real data\n- Exercise Presentation / visual editing against the local preview iframe\n\n### Inspecting Production Builds with Vite DevTools\n\nThe test studio can run with [Vite DevTools](https://devtools.vite.dev) enabled, which lets you inspect the output of `sanity build` runs (module graph, chunks, plugin timings, bundle treemaps, session diffing) from inside a long-running `sanity dev` server—no restart needed.\n\n```bash\n# Builds the test studio with devtools enabled, then starts the dev server\n# (so there's a build session to inspect right away)\npnpm devtools:test-studio\n```\n\nOpen `http://localhost:3333` and use the Vite DevTools dock to explore the recorded Rolldown build session. See the [DevTools for Rolldown features guide](https://devtools.vite.dev/rolldown/features.html) for how to use the module graph, chunk, asset, and plugin panels.\n\nTo inspect a **new** build after making changes—while `pnpm devtools:test-studio` is still running—run in a second terminal:\n\n```bash\n# Creates a fresh build session that shows up in the running DevTools dock\npnpm devtools:test-studio:build\n```\n\nBuilds are not hooked into HMR; `sanity build` must be invoked manually (via the command above) each time you want a new session to inspect. Sessions can be compared against each other in the DevTools UI to diff bundle changes.\n\nHow it works:\n\n- Both commands set `ENABLE_VITE_DEVTOOLS=true`, which makes `dev/test-studio/sanity.cli.ts` add the `DevTools()` Vite plugin and enable `build.rolldownOptions.devtools`\n- Build sessions are written to `dev/test-studio/node_modules/.rolldown` (gitignored)\n- The flag is declared in `dev/test-studio/turbo.json` so turbo-cached builds are invalidated when it changes\n- Enabling devtools makes `sanity build` noticeably slower; that's why it's opt-in via the env flag\n\n### Studio performance benchmarks (perf/bench — No Auth Required)\n\nThe `perf/bench` suite benchmarks a built studio against a **local mock** of the Sanity API — fully hermetic, no tokens, no network:\n\n```bash\npnpm build:bench                                   # build packages + bench studio (required first)\npnpm bench help                                    # list all bench CLI commands\npnpm bench run --scenario singleString             # absolute interaction benchmark\npnpm bench run --mode pageload --scenario singleString  # load vitals + bundle size\npnpm bench:unit                                    # mock-contract + stats unit tests\npnpm bench dev                                     # mock + `sanity dev` for interactive debugging\n```\n\nSee `perf/bench/README.md` for A/B comparisons, scenarios, and CI details. `dev/efps` is the legacy perf suite, kept for reference while perf/bench burns in.\n\n### E2E Tests (Token Required)\n\nE2E tests require authentication tokens. Add these to `.env.local` in the repo root:\n\n```bash\nSANITY_E2E_SESSION_TOKEN=<your-token>\nSANITY_E2E_PROJECT_ID=<project-id>\nSANITY_E2E_DATASET=<dataset-name>\n```\n\n**How to get a token:**\n\n```bash\n# Option 1: Use your CLI token\nsanity login\nsanity debug --secrets  # Look for \"Auth token\"\n\n# Option 2: Create a project token at https://sanity.io/manage\n# Navigate to: Project Settings → API → Tokens → Add API token\n```\n\nThen run E2E tests:\n\n```bash\npnpm e2e:build              # Build E2E studio\npnpm test:e2e               # Run E2E tests\npnpm test:e2e --ui          # Interactive mode\n```\n\n**Note:** E2E tests are typically run in CI, not locally during development. Most changes can be verified with unit tests.\n\n### Important Note for AI Agents\n\n**What requires authentication:**\n\n- Running the dev studio (`pnpm dev`)\n- E2E tests (`pnpm test:e2e`)\n- Any command that connects to Sanity APIs\n\n**What does NOT require authentication:**\n\n- Building packages (`pnpm build`)\n- Running unit tests (`pnpm test`)\n- Linting, formatting, and type checking (`pnpm lint`, `pnpm lint:fix`, `pnpm check:oxlint`)\n\n**Recommendation:** For most code changes, use `pnpm build && pnpm test` to verify correctness. This covers the vast majority of development tasks without any auth setup. Only use the dev studio when visual verification is specifically needed.\n\n## Coding Standards\n\nCoding standards are enforced by **oxlint** (native Rust rules, type-aware rules via tsgolint, TypeScript type checking via `options.typeCheck`, and a few ESLint plugins loaded through oxlint's `jsPlugins`). TypeScript type checking is included in `pnpm lint` / `pnpm check:oxlint` — no separate `tsc` step. Check your code with:\n\n```bash\npnpm lint              # Check for issues (oxlint, includes type checking)\npnpm lint:fix          # Auto-fix issues (oxfmt + oxlint --fix)\n```\n\nAll packages use **ESM** (`\"type\": \"module\"`). TypeScript strict mode is enabled.\n\nRules that the linter already enforces (restricted imports, type-aware rules, React Compiler rules, i18n rules, module boundaries) are not repeated in this guide — run `pnpm lint` and follow the reported messages, which explain the expected pattern.\n\n### Do Not Weaken the Linter\n\nFix the reported problem instead of silencing it. In order of preference:\n\n1. **Fix the code** so the rule passes. This is almost always the right answer.\n2. **Suppress the single line** as a last resort, when the rule is genuinely wrong for that one spot: `// oxlint-disable-next-line <rule> -- <why>`. Always name the specific rule and explain the exception after `--`. Never suppress a rule merely to make CI green.\n3. **Change `.oxlintrc.json` only when a human explicitly asks.** Do not turn rules off, downgrade severity, add `overrides` entries, or widen `ignorePatterns` on your own initiative — an override silences the rule for every current and future file it matches. If you think a rule is wrong, leave it failing and raise it in your summary or the PR description.\n\nFile-wide `/* oxlint-disable <rule> */` is reserved for files that are an exception as a whole — vendored code, the `packages/sanity/src/ui-components` wrappers around raw `@sanity/ui`, CLI scripts that print to `console`. Follow that existing precedent rather than reaching for it to clear a handful of errors.\n\n`options.reportUnusedDisableDirectives` is `error`, so a suppression that stops being necessary fails CI — drop suppressions when the code underneath them changes.\n\n### Effect events: use `use-effect-event`, not React's native hook\n\nImport `useEffectEvent` from `use-effect-event`, never from `react`. On React 19.2 the native hook\nreturns first-render values when the calling component is wrapped in `forwardRef` or `memo`\n([facebook/react#34818](https://github.com/facebook/react/issues/34818), fixed in 19.3 canaries).\n`eslint/no-restricted-imports` in `.oxlintrc.json` enforces this. The bug reaches any dependency that\nwraps the native hook, so check the implementation before trusting one — `react-rx` is safe on both\nv4 and v5 because `useObservableEvent` builds on the same `use-effect-event` ponyfill.\n\n### Refs: use `props.ref`, not `forwardRef`\n\nReact 19 passes `ref` as a regular prop. Do not use `forwardRef` — destructure `ref` from props\n(so it is not left in a `...rest` spread) and forward it like any other prop.\n`eslint/no-restricted-imports` bans importing `forwardRef` from `react`.\n\nPrefer a named function declaration over `const X = function …` / arrow wrappers:\n\n```ts\n// preferred\nexport function MyComponent(props: Props & RefAttributes<HTMLDivElement>) {\n  const {ref, ...rest} = props\n  return <div ref={ref} {...rest} />\n}\n\n// avoid\nexport const MyComponent = function MyComponent(props: …) { … }\nexport const MyComponent = (props: …) => { … }\n```\n\nWhen wrapping with `memo`, declare the component as a function first, then memoize:\n\n```ts\nfunction MyComponent(props: …) { … }\nexport const MyComponentMemo = memo(MyComponent)\n```\n\nFor typings, include `ref` on the props type: stop omitting `'ref'` from `HTMLProps` /\n`ComponentProps`, or intersect with `RefAttributes<T>`. Avoid `PropsWithRef` — in `@types/react`\n19 it is a deprecated identity alias and trips `typescript/no-deprecated`.\n\n## Testing\n\n### Unit Tests (Vitest)\n\n```bash\npnpm test                    # Run all tests\npnpm test -- --watch        # Watch mode\npnpm test -- -u             # Update snapshots\npnpm test -- --project=sanity  # Run specific project\n```\n\nTests require a build first because some tests use compiled output:\n\n```bash\npnpm build && pnpm test\n```\n\n#### Test Timeouts\n\nWhen a test needs a custom timeout, use the Vitest options object as the second argument (not the deprecated third-argument form). Prefer numeric separators for readability:\n\n```ts\n// Correct\ntest('my test', {timeout: 30_000}, async () => {\n  // ...\n})\n\n// Wrong — timeout as third argument (deprecated)\ntest('my test', async () => {\n  // ...\n}, 30000)\n```\n\n#### Testing components that suspend via `use()`\n\nTwo traps when unit testing a component or hook that suspends on a promise with React's `use()`\n(see `packages/sanity/src/presentation/__tests__/useMainDocumentPolyfill.test.tsx`):\n\n- **Mount inside an awaited async `act`.** `render`/`renderHook` wrap the mount in an internal\n  _synchronous_ `act`, and React refuses to resume work that suspended inside an unawaited `act`\n  scope — the suspended tree parks forever and `waitFor` times out. Wrap the mount yourself:\n  `await act(async () => { renderHook(...) })` (suppress `testing-library/no-unnecessary-act` on\n  that line; this is the exception the rule doesn't know about). A `Suspense` wrapper is also\n  required.\n- **Keep the `use()` call sequence stable across the replay.** After the promise settles, React\n  _replays_ the suspended render reusing the recorded hook state. If the awaited promise's side\n  effect flips the condition guarding a conditional `use()` (e.g. a polyfill import that installs\n  a global the condition checks), the replay skips the `use()` call, hook accounting breaks, and\n  React throws `Update hook called on initial render` as a recoverable error — which vitest can\n  catch as an unhandled error and fail the run. Once a load has started, keep calling `use()` on\n  the same cached promise on every render instead of re-checking the environment.\n\n#### Vanilla-extract in jsdom tests\n\nThe `sanity` and `@sanity/vision` jsdom suites import\n[`@vanilla-extract/css/disableRuntimeStyles`](https://vanilla-extract.style/documentation/test-environments/#disabling-runtime-styles)\n(`packages/sanity/test/setup/environment.ts`, and as a direct vitest `setupFiles` entry in\n`packages/@sanity/vision/vitest.config.mts`), so vanilla-extract skips injecting real stylesheets\ninto jsdom. Class name identifiers still resolve, but computed styles are not available.\n\nConventions that follow from this:\n\n- **Do not assert on vanilla-extract class names or computed styles in jsdom tests.** Assert on\n  `data-testid` attributes instead. Visual/style behavior belongs in the vitest browser mode\n  suite (`*.browser.test.tsx`, real Chromium/Firefox/WebKit) or the Playwright e2e tests, where\n  runtime styles stay enabled.\n- **Keep `vanillaExtractPlugin()` in the vitest configs.** The plugin's transform assigns file\n  scopes to `.css.ts` modules; without it any test that (transitively) imports a `.css.ts` file\n  throws \"Styles were unable to be assigned to a file\". `disableRuntimeStyles` only skips style\n  injection, not the transform.\n\n#### @sanity/ui overlays stay mounted when closed\n\nFrom `@sanity/ui` v4, Tooltip/Popover/Menu keep their content mounted via React `<Activity>`\nwhile closed (hidden with `display: none`). Consequences for tests:\n\n- Plain text / test-id queries can match **closed** overlay content. Prefer scoping to the\n  visible element under test (or assert visibility) instead of `getByText` / `getByTestId` on\n  the whole document.\n- In jsdom, asserting that closed content is hidden works (`expect(...).not.toBeVisible()`), but\n  selecting the **open** overlay by visibility does not. Runtime styles are disabled there, so\n  nothing overrides the `hidden` attribute `@sanity/ui` puts on an open popover, and\n  `getByRole` (which skips inaccessible nodes) finds neither the open nor the closed copy. Pick\n  the open one by the absence of the `display: none` that `<Activity>` applies to closed\n  overlays, rather than by index:\n\n  ```ts\n  const [openMenu] = getAllByDataUi(document.body, 'MenuButton__popover').filter(\n    (popover) => popover.style.display !== 'none',\n  )\n  const item = within(openMenu).getByRole('menuitem', {name: 'Discard version', hidden: true})\n  ```\n\n  Selecting with `getAllByText(...)[0]` also works, but silently depends on portal ordering.\n  Visibility-based selection belongs in the browser-mode suite, where real styles apply and\n  `checkVisibility()` is meaningful.\n\n- Test routers must include intent routes (`route.create('/', [route.intents('/intent')])`).\n  Reference item menus render `IntentLink` (\"Open in new tab\") even while closed; without\n  intent routes, `resolveIntentLink` throws during render and the form subtree disappears.\n  See `packages/sanity/test/browser/TestWrapper.tsx` and `test/testUtils/TestProvider.tsx`.\n\n### Visual Regression Tests (Chromatic + Storybook)\n\nVisual regression runs on Chromatic via `.github/workflows/chromatic.yml`. `dev/storybook`\ncontains the stories — most reuse the vitest browser-mode test harnesses (`TestWrapper` +\n`*Story.tsx` components), plus authored migration sentinels for `ui-components` and\nvanilla-extract-migrated components.\n\n```bash\npnpm dev:storybook                    # Storybook dev server at http://localhost:6006\npnpm build:storybook                  # Static build via turbo (dev/storybook/storybook-static)\npnpm --filter sanity-storybook test   # Run every story as a vitest browser-mode test\nCHROMATIC=1 pnpm --filter sanity test:browser   # Chromatic archive capture run (chromium only)\n```\n\nRepo secrets: `CHROMATIC_PROJECT_TOKEN_STORYBOOK` (active), `CHROMATIC_PROJECT_TOKEN_E2E`\n(active, used by e2e), `CHROMATIC_PROJECT_TOKEN_VITEST` (dormant until Chromatic's Vitest early\naccess is enabled — the CI job self-activates when the secret is added). Checks are non-gating\nduring burn-in. See the `sanity-visual-regression` skill\n(`.agents/skills/sanity-visual-regression/SKILL.md`) for how to add coverage, determinism rules,\nand the Vitest activation runbook.\n\n### E2E Tests (Playwright)\n\n```bash\npnpm e2e:build              # Build E2E studio\npnpm test:e2e               # Run E2E tests\npnpm test:e2e --ui          # Interactive mode\n```\n\n## Pre-commit Hook\n\nLefthook runs on commit (see `lefthook.yml`), which:\n\n1. Runs oxfmt on staged files\n2. Runs oxlint `--fix` on staged `.js/.ts/.tsx` files (with `--no-error-on-unmatched-pattern` so packages in oxlint `ignorePatterns`, e.g. `@repo/test-dts-exports`, can still be committed)\n\nIf the hook fails, run `pnpm lint:fix` to fix issues.\n\n## Common Tasks\n\n### Adding a New Dependency\n\n```bash\n# Add to specific package\npnpm --filter sanity add <package>\n\n# Add to root (dev dependency)\npnpm add -w -D <package>\n```\n\n### Creating a New Test\n\n1. Create test file next to source: `MyComponent.test.tsx`\n2. Use existing test patterns from similar files\n3. Run `pnpm test -- MyComponent` to verify\n\n### Updating Snapshots\n\nWhen making intentional changes that affect snapshots:\n\n```bash\n# Update all snapshots\npnpm test -- -u\n\n# Update specific test's snapshots\npnpm test -- -u MyComponent\n```\n\nReview snapshot changes carefully before committing.\n\n## Commit Message Format and PR Title (CRITICAL)\n\nThis repo uses **conventional commits** for automated releases.\n\n**PR titles are validated by CI** using the [semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) action. A PR with a non-conforming title **will fail CI**.\n\n### Format\n\n```\ntype(scope): lowercase description without special characters\n```\n\n### Rules\n\n1. **Type** is required and must be one of: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `ci`\n2. **Scope** is required and should be the package or area affected (e.g., `groq`, `cli`, `form`, `deps`)\n3. **Description** must start with a lowercase letter\n4. **No backticks, quotes, or markdown** in the PR title — keep it plain text\n5. Use `fix` for bug fixes, `feat` for new features, `chore` for maintenance tasks\n\n### Choosing the Right Type\n\n- **`fix`** — Fixes a bug or resolves an issue (e.g., `fix(groq): resolve CJS type export issue`)\n- **`feat`** — Adds new functionality (e.g., `feat(form): add array input component`)\n- **`chore`** — Maintenance, dependency updates, CI changes (e.g., `chore(deps): update dependencies`)\n- **`docs`** — Documentation only (e.g., `docs(readme): improve installation instructions`)\n- **`refactor`** — Code restructuring without behavior change (e.g., `refactor(store): simplify document subscription logic`)\n- **`test`** — Adding or updating tests (e.g., `test(validation): add edge case coverage`)\n- **`perf`** — Performance improvements (e.g., `perf(search): optimize query execution`)\n- **`ci`** — CI/CD changes (e.g., `ci(e2e): add retry logic to flaky tests`)\n\n### Examples\n\n```\n# ✅ Good PR titles\nfix(groq): resolve CJS type export issue\nfeat(form): add new array input component\nchore(deps): update dependencies\n\n# ❌ Bad PR titles\nfeat(groq): add `types` condition     # no backticks allowed\nFix(cli): Handle missing config        # type must be lowercase, description must start lowercase\nadded new feature                       # missing type and scope\n```\n\n## Pull Request Workflow\n\n### 1. Create as Draft PR First\n\n**Always create PRs as drafts first.** The prompter (person who requested the work) reviews before the broader team.\n\n```bash\n# Create a draft PR — title MUST follow conventional commit format\ngh pr create --draft --title \"fix(scope): description\" --body \"...\" --label \"🤖 bot\"\n```\n\n### 2. Apply the \"🤖 bot\" Label\n\n**All PRs created by AI agents must be labeled with `🤖 bot`.** This label already exists on the repo and helps the team identify agent-created PRs for tracking and review workflows.\n\nWhen creating or updating a PR, always ensure the label is applied. If the create command did not accept `--label`, add it afterward:\n\n```bash\ngh pr edit --add-label \"🤖 bot\"\n```\n\n### 3. Move Out of Draft\n\nOnce the prompter approves and CI is green, convert from draft to ready-for-review:\n\n```bash\ngh pr ready\n```\n\n### 4. What Not To Touch Unless Asked\n\n- **`.github/CODEOWNERS`** — do not add or change ownership rules unless explicitly requested\n- **Release automation / version bumps** — versioning is driven by conventional commits on merge; do not open manual version PRs unless asked\n\n### Useful PR Labels\n\n| Label                | When to use                                                                          |\n| -------------------- | ------------------------------------------------------------------------------------ |\n| `🤖 bot`             | **Required** on every AI-agent PR                                                    |\n| `trigger: preview`   | Publishes preview packages via [`pkg.pr.new`](https://pkg.pr.new) (maintainer-gated) |\n| `trigger:perf-bench` | Runs the `perf/bench` suite on the PR (maintainer-gated)                             |\n| `full-test-suite`    | Forces the full unit test suite to run                                               |\n\nDo **not** apply `trigger:*` labels unless the prompter or a maintainer asks — they kick off expensive or publish workflows.\n\n### Crediting Original Authors (Ported / Cherry-picked Work)\n\nWhen porting or rebasing someone else's PR (community contribution, backport, etc.), credit the **original author**, not only the agent or whoever opens the port PR:\n\n1. Prefer commits authored as the original contributor when history allows:\n\n   ```bash\n   git commit --author=\"their-name <their-github-email>\" -m \"...\"\n   ```\n\n2. Otherwise add a `Co-authored-by:` trailer (and mention them in the PR description / Notes for release):\n\n   ```\n   Co-authored-by: Their Name <their-github-noreply@users.noreply.github.com>\n   ```\n\nWorkflow summary:\n\n1. **Agent creates draft PR** with the `🤖 bot` label\n2. **Prompter reviews** the draft\n3. **Mark ready for review** once the prompter approves\n4. **Team reviews** and merges\n\nThis ensures the person who prompted the changes can verify correctness before involving the broader team.\n\n## Keeping This Guide Updated\n\n**If you're asked to do something not documented here, update this file.**\n\nWhen working on a PR and you're asked to:\n\n- Run a command that isn't in this guide\n- Follow a workflow that isn't documented\n- Fix something using a non-obvious process\n\nAdd that knowledge to this `AGENTS.md` file as part of the same PR. This keeps the guide accurate and helps future agents (and humans) avoid the same gaps.\n\nExample: If asked \"run the e2e tests for just the form inputs\", and that's not documented, add it to the Testing section before completing the task.\n\n## Troubleshooting\n\n### Build Issues\n\n```bash\n# Clean everything and rebuild\npnpm clean && pnpm install && pnpm build\n```\n\n### Test Failures\n\n1. Ensure you've built: `pnpm build`\n2. Check if snapshots need updating: `pnpm test -- -u`\n3. Run specific test for better output: `pnpm test -- <test-name>`\n\n### Lint Failures\n\n```bash\n# Fix all lint issues\npnpm lint:fix\n\n# Check what would be fixed (dry run)\npnpm check:format\npnpm check:oxlint\n```\n\n## Environment Variables\n\nKey env vars used in development:\n\n- `SANITY_STUDIO_PROJECT_ID` - Project ID for dev studio\n- `SANITY_STUDIO_DATASET` - Dataset for dev studio\n- `SANITY_INTERNAL_ENV` - Internal environment flag\n\nSee `turbo.json` for full list of environment variables that affect builds.\n\n## Useful Links\n\n- [CONTRIBUTING.md](./CONTRIBUTING.md) - Full contribution guidelines\n- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) - Community guidelines\n- [packages/sanity/README.md](./packages/sanity/README.md) - Main package docs\n\n## Cursor Cloud specific instructions\n\nThese notes cover non-obvious gotchas for running in the Cursor Cloud VM. The startup update script already runs `pnpm install`.\n\n### Services\n\n| Service                                           | Port | Purpose                                          |\n| ------------------------------------------------- | ---- | ------------------------------------------------ |\n| Test studio (`pnpm dev` / `pnpm dev:test-studio`) | 3333 | Local Sanity Studio for manual verification      |\n| Preview iframe (`pnpm dev:preview-iframe`)        | 3334 | Cross-origin Presentation preview (vanilla Vite) |\n| Storybook (`pnpm dev:storybook`)                  | 6006 | Visual regression stories (Chromatic)            |\n\nNo Docker, databases, or other local services are required for unit tests, lint, or build. CI-style verification (`pnpm lint`, `pnpm build`, `pnpm test`) runs entirely in-process.\n\n### Gotchas\n\n- **Root `typescript` is TypeScript 7.** Catalog `typescript` (^7) is a normal root dependency and provides the native `tsc` binary for vitest typecheck (`*.test-d.*`) and for tsdown `dts: {tsgo: true}` (packages also declare catalog `typescript`). CI type checking of application code is owned by oxlint (`options.typeCheck`). Tools that still need the TypeScript 6 compiler API keep that isolated: `@repo/typedoc` (typedoc) and `@repo/test-dts-exports` (ts-morph) depend on `typescript` aliased to `@typescript/typescript6`. The old symlink workaround for a missing root `tsc` is no longer needed.\n- **Dev studio auth for cloud agents — use the `STUDIO_AUTH_TOKEN` secret, not interactive login.** `pnpm dev` runs `sanity dev --no-auto-updates` (non-interactive, no upgrade prompt) and serves the app at `http://localhost:3333`. The test studio connects to Sanity Cloud (project `ppsg7ml5`); its default workspace is `/test`. Without auth the workspaces show \"Signed out\" / \"Choose login provider\". To authenticate, put the injected `STUDIO_AUTH_TOKEN` in the URL hash — Sanity consumes it on load and strips it from the address bar:\n  - Build the URL: `node -e \"console.log('http://localhost:3333/test#token=' + encodeURIComponent(process.env.STUDIO_AUTH_TOKEN))\"` (any workspace basePath works, e.g. `/test`).\n  - Because the Read tool redacts the token, you cannot paste the URL into browser instructions directly. A reliable trick is a tiny local HTTP server that reads `STUDIO_AUTH_TOKEN` from env and serves an HTML page doing `location.replace(<studio-url-with-token>)`, then point the browser at that server (keeps the secret out of prompts/screenshots). After load you land authenticated in the workspace and can create/publish documents (e.g. an `Author`).\n  - Most changes should still be verified with `pnpm build && pnpm test` (no auth needed); only use the studio for visual/manual verification.\n- **Seeding test documents for the `/test` workspace via API.** In local dev (non-staging), the `/test` workspace talks to the production API host, so `STUDIO_AUTH_TOKEN` works as a Bearer token against `https://ppsg7ml5.api.sanity.io/v2024-01-01/data/mutate/test` (it returns 401 \"Session not found\" on `api.sanity.work`). Caveat when testing history/review-changes features: documents created by raw API mutations (e.g. `createOrReplace` of a published id) do not produce publish events, so the Review changes inspector shows \"There are no changes\" / \"Same revision selected\". Instead, create only the draft (`drafts.<id>`) via the API, click Publish in the studio UI to create a real publish event, then edit fields in the form to create draft changes.\n- **Seeding releases for the `/test` workspace via API.** Releases and document versions are created through the actions endpoint (`POST https://ppsg7ml5.api.sanity.io/v2025-02-19/data/actions/test` with `{\"actions\": [...]}`, same Bearer token). Useful action types: `sanity.action.release.create`, `sanity.action.document.version.create` (pass `publishedId` plus a `document` with `_id: versions.<releaseId>.<publishedId>`), `sanity.action.document.version.unpublish`, `sanity.action.document.version.discard`, `sanity.action.release.archive`, `sanity.action.release.delete`. Note that a version created by the unpublish action alone is an empty tombstone carrying only `_system.delete: true` — to get a version with content, create the version first and then unpublish it. `/test` is a shared dataset, so archive and delete any release you seed once you are done.\n- **Node version:** the VM runs Node 22.x, which satisfies the repo engine range (`>=22.12`). A couple of internal tooling packages print a harmless `Unsupported engine` warning wanting Node `>=22.18`; it does not affect testing or running the studio. However, **`pnpm build` requires Node >= 22.18**: the packages build with `tsdown`, which loads its `tsdown.config.ts` through Node's native TypeScript support and fails on older Node 22.x (e.g. the VM default `v22.14.0`) with `Failed to import module \"unrun\"`. A new enough runtime is available via nvm: `export PATH=\"$HOME/.nvm/versions/node/v22.22.2/bin:$PATH\"`.\n- **`pnpm build` may dirty `packages/sanity/package.json`.** tsdown auto-generates the `inlinedDependencies` field on every build, and in this VM the computed set can differ from what is committed (e.g. `@sanity/sdk` and `zustand` get dropped) even on a clean checkout of `main`. That churn is an environment artifact, not part of your change — revert it with `git checkout -- packages/sanity/package.json` (re-applying any edits of your own) instead of committing it.\n- **Do not run oxlint type checking (`pnpm check:oxlint`) while the dev studio is running.** Both are memory-hungry and running them concurrently has exhausted the VM's memory and frozen it for hours (unkillable thrashing). Stop `sanity dev` first (Ctrl-C in its tmux session), run the checks, then restart the studio.\n"},"files":{"AGENTS.md":"# AGENTS.md - AI Agent Guidelines for Sanity Monorepo\n\nThis document helps AI agents work successfully with the Sanity monorepo.\n\n> **Self-Improvement:** If you discover undocumented requirements, commands, or workflows during your work (e.g., a reviewer asks you to run something not covered here), update this file on the same PR. Keep this guide accurate and helpful for future agents.\n\n## Prerequisites\n\n- **Node.js**: v24 or latest LTS\n- **Package Manager**: pnpm v10+ (exact version managed via `packageManager` field in package.json)\n\n## Quick Reference\n\n```bash\n# Install dependencies (pnpm ONLY - enforced)\npnpm install\n\n# Build all packages (required before testing)\npnpm build\n\n# Run dev studio (requires auth, see below)\npnpm dev\n\n# Format code (MUST pass CI)\npnpm chore:format:fix\n\n# Fix all lint issues (MUST pass CI) — includes TypeScript type checking via oxlint\npnpm lint:fix\n\n# Run tests\npnpm test\n\n# Update snapshots if tests fail due to expected changes\npnpm test -- -u\n\n# Lint + type check (oxlint typeAware + typeCheck; no separate tsc step)\npnpm check:oxlint\n```\n\n## CI Checks - What Must Pass\n\nThese checks run on every PR and **must pass**:\n\n| Check            | Command               | Notes                                                                                                                                                            |\n| ---------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Format**       | `pnpm check:format`   | Uses oxfmt. Fix with `pnpm chore:format:fix`                                                                                                                     |\n| **Oxlint**       | `pnpm check:oxlint`   | Rust linter with type-aware rules and TypeScript type checking via tsgolint (`options.typeCheck`). Fix with `pnpm chore:oxlint:fix`                              |\n| **Unit Tests**   | `pnpm test`           | Vitest, sharded in CI                                                                                                                                            |\n| **Export Tests** | `pnpm test:exports`   | Ensures ESM/CJS/DTS work                                                                                                                                         |\n| **Dep Check**    | `pnpm depcheck`       | Finds unused/missing deps                                                                                                                                        |\n| **Zizmor**       | `pnpm lint:workflows` | Audits `.github/workflows/` for security issues. Fails CI on high-severity findings. Local run needs [`zizmor`](https://docs.zizmor.sh/installation/) on `PATH`. |\n| **PR Title**     | Conventional commits  | e.g., `feat(scope): description`                                                                                                                                 |\n\n### Before Committing\n\nRun these commands to avoid CI failures:\n\n```bash\n# Fix all formatting and lint issues\npnpm lint:fix\n\n# Verify tests pass (build first if needed)\npnpm build && pnpm test\n```\n\nIf tests fail due to **expected snapshot changes**, update them:\n\n```bash\npnpm test -- -u\n```\n\nSnapshot files are located in `__snapshots__` directories alongside test files.\n\n## Project Structure\n\n```\nsanity/\n├── packages/\n│   ├── sanity/           # Main Sanity studio package\n│   ├── @sanity/          # Scoped packages (cli, types, schema, etc.)\n│   └── @repo/            # Internal tooling (test-config, tsconfig, etc.)\n├── dev/                  # Development studios for testing\n│   ├── test-studio/      # Primary dev studio (pnpm dev runs this)\n│   └── preview-iframe/   # Presentation preview iframe (vanilla Vite, port 3334)\n├── e2e/                  # End-to-end Playwright tests\n├── perf/                 # Performance testing\n└── examples/             # Example studios\n```\n\n### Key Packages\n\n- **`packages/sanity`** - Core studio package with all UI components\n- **`packages/@sanity/types`** - TypeScript type definitions\n- **`packages/@sanity/schema`** - Schema compilation\n- **`packages/@sanity/mutator`** - Document mutation logic\n\n## Build System\n\n- **Package Manager**: pnpm (version 10.x, enforced via `preinstall`)\n- **Build Orchestration**: Turbo (caches builds)\n- **Versioning**: Lerna-lite with conventional commits\n\n### Build Commands\n\n```bash\npnpm build              # Build all packages\npnpm watch              # Watch mode for development\n```\n\n### Running the Dev Studio\n\n```bash\npnpm dev                # Starts dev studio at http://localhost:3333\n```\n\n**Note:** The dev studio requires Sanity user authentication in the browser. It's a Vite application that communicates with Sanity API endpoints, so you'll need to log in with a Sanity account when you access `http://localhost:3333` to use the studio.\n\n## Local Development\n\nThis section clarifies what requires authentication and what doesn't—critical for AI agents to avoid getting stuck on auth flows.\n\n### Running Tests (No Auth Required)\n\nUnit tests run in jsdom with mocks and **do not require any authentication**:\n\n```bash\n# Build first (required), then run all tests\npnpm build && pnpm test\n\n# Run a single test file (IMPORTANT: use vitest directly with --project to avoid running all tests)\npnpm vitest run --project=sanity packages/sanity/src/core/hooks/useClient.test.ts\n\n# Run a single test file with verbose output\npnpm vitest run --project=sanity --reporter=verbose packages/sanity/src/core/hooks/useClient.test.ts\n\n# Watch mode for iterative development\npnpm test -- --watch\n\n# Run tests for a specific package\npnpm test -- --project=sanity\n```\n\n**Important:** Do NOT use `pnpm test -- path/to/file.test.ts` for running a single file — it runs all tests across all projects. Use `pnpm vitest run --project=<project> <path>` instead.\n\nComponents that need auth context use `createMockAuthStore` in tests, so no real authentication is needed. This is the recommended way to verify most code changes.\n\n### Running the Dev Studio (Auth Required)\n\n```bash\npnpm dev  # Starts test-studio at http://localhost:3333 and preview-iframe at http://localhost:3334\n```\n\n- **Requires browser authentication** on first visit—you'll be prompted to log in with a Sanity account\n- Connects to a real Sanity project (configured in `dev/test-studio/sanity.config.ts`)\n- Uses staging API by default (`api.sanity.work`)\n- Session persists in browser, so subsequent visits won't require re-authentication\n- `pnpm dev` / `pnpm dev:test-studio` also starts `dev/preview-iframe` (vanilla Vite on port 3334) so Presentation can load its cross-origin iframe. Studio-only: `pnpm dev:test-studio:studio`. Preview-only: `pnpm dev:preview-iframe`.\n- Deployed preview iframe: Sanity Sandbox Vercel project `test-studio-preview-iframe` (`https://test-studio-preview-iframe.sanity.dev`)\n\nUse the dev studio when you need to:\n\n- Visually verify UI changes\n- Test real document editing workflows\n- Debug issues that only appear with real data\n- Exercise Presentation / visual editing against the local preview iframe\n\n### Inspecting Production Builds with Vite DevTools\n\nThe test studio can run with [Vite DevTools](https://devtools.vite.dev) enabled, which lets you inspect the output of `sanity build` runs (module graph, chunks, plugin timings, bundle treemaps, session diffing) from inside a long-running `sanity dev` server—no restart needed.\n\n```bash\n# Builds the test studio with devtools enabled, then starts the dev server\n# (so there's a build session to inspect right away)\npnpm devtools:test-studio\n```\n\nOpen `http://localhost:3333` and use the Vite DevTools dock to explore the recorded Rolldown build session. See the [DevTools for Rolldown features guide](https://devtools.vite.dev/rolldown/features.html) for how to use the module graph, chunk, asset, and plugin panels.\n\nTo inspect a **new** build after making changes—while `pnpm devtools:test-studio` is still running—run in a second terminal:\n\n```bash\n# Creates a fresh build session that shows up in the running DevTools dock\npnpm devtools:test-studio:build\n```\n\nBuilds are not hooked into HMR; `sanity build` must be invoked manually (via the command above) each time you want a new session to inspect. Sessions can be compared against each other in the DevTools UI to diff bundle changes.\n\nHow it works:\n\n- Both commands set `ENABLE_VITE_DEVTOOLS=true`, which makes `dev/test-studio/sanity.cli.ts` add the `DevTools()` Vite plugin and enable `build.rolldownOptions.devtools`\n- Build sessions are written to `dev/test-studio/node_modules/.rolldown` (gitignored)\n- The flag is declared in `dev/test-studio/turbo.json` so turbo-cached builds are invalidated when it changes\n- Enabling devtools makes `sanity build` noticeably slower; that's why it's opt-in via the env flag\n\n### Studio performance benchmarks (perf/bench — No Auth Required)\n\nThe `perf/bench` suite benchmarks a built studio against a **local mock** of the Sanity API — fully hermetic, no tokens, no network:\n\n```bash\npnpm build:bench                                   # build packages + bench studio (required first)\npnpm bench help                                    # list all bench CLI commands\npnpm bench run --scenario singleString             # absolute interaction benchmark\npnpm bench run --mode pageload --scenario singleString  # load vitals + bundle size\npnpm bench:unit                                    # mock-contract + stats unit tests\npnpm bench dev                                     # mock + `sanity dev` for interactive debugging\n```\n\nSee `perf/bench/README.md` for A/B comparisons, scenarios, and CI details. `dev/efps` is the legacy perf suite, kept for reference while perf/bench burns in.\n\n### E2E Tests (Token Required)\n\nE2E tests require authentication tokens. Add these to `.env.local` in the repo root:\n\n```bash\nSANITY_E2E_SESSION_TOKEN=<your-token>\nSANITY_E2E_PROJECT_ID=<project-id>\nSANITY_E2E_DATASET=<dataset-name>\n```\n\n**How to get a token:**\n\n```bash\n# Option 1: Use your CLI token\nsanity login\nsanity debug --secrets  # Look for \"Auth token\"\n\n# Option 2: Create a project token at https://sanity.io/manage\n# Navigate to: Project Settings → API → Tokens → Add API token\n```\n\nThen run E2E tests:\n\n```bash\npnpm e2e:build              # Build E2E studio\npnpm test:e2e               # Run E2E tests\npnpm test:e2e --ui          # Interactive mode\n```\n\n**Note:** E2E tests are typically run in CI, not locally during development. Most changes can be verified with unit tests.\n\n### Important Note for AI Agents\n\n**What requires authentication:**\n\n- Running the dev studio (`pnpm dev`)\n- E2E tests (`pnpm test:e2e`)\n- Any command that connects to Sanity APIs\n\n**What does NOT require authentication:**\n\n- Building packages (`pnpm build`)\n- Running unit tests (`pnpm test`)\n- Linting, formatting, and type checking (`pnpm lint`, `pnpm lint:fix`, `pnpm check:oxlint`)\n\n**Recommendation:** For most code changes, use `pnpm build && pnpm test` to verify correctness. This covers the vast majority of development tasks without any auth setup. Only use the dev studio when visual verification is specifically needed.\n\n## Coding Standards\n\nCoding standards are enforced by **oxlint** (native Rust rules, type-aware rules via tsgolint, TypeScript type checking via `options.typeCheck`, and a few ESLint plugins loaded through oxlint's `jsPlugins`). TypeScript type checking is included in `pnpm lint` / `pnpm check:oxlint` — no separate `tsc` step. Check your code with:\n\n```bash\npnpm lint              # Check for issues (oxlint, includes type checking)\npnpm lint:fix          # Auto-fix issues (oxfmt + oxlint --fix)\n```\n\nAll packages use **ESM** (`\"type\": \"module\"`). TypeScript strict mode is enabled.\n\nRules that the linter already enforces (restricted imports, type-aware rules, React Compiler rules, i18n rules, module boundaries) are not repeated in this guide — run `pnpm lint` and follow the reported messages, which explain the expected pattern.\n\n### Do Not Weaken the Linter\n\nFix the reported problem instead of silencing it. In order of preference:\n\n1. **Fix the code** so the rule passes. This is almost always the right answer.\n2. **Suppress the single line** as a last resort, when the rule is genuinely wrong for that one spot: `// oxlint-disable-next-line <rule> -- <why>`. Always name the specific rule and explain the exception after `--`. Never suppress a rule merely to make CI green.\n3. **Change `.oxlintrc.json` only when a human explicitly asks.** Do not turn rules off, downgrade severity, add `overrides` entries, or widen `ignorePatterns` on your own initiative — an override silences the rule for every current and future file it matches. If you think a rule is wrong, leave it failing and raise it in your summary or the PR description.\n\nFile-wide `/* oxlint-disable <rule> */` is reserved for files that are an exception as a whole — vendored code, the `packages/sanity/src/ui-components` wrappers around raw `@sanity/ui`, CLI scripts that print to `console`. Follow that existing precedent rather than reaching for it to clear a handful of errors.\n\n`options.reportUnusedDisableDirectives` is `error`, so a suppression that stops being necessary fails CI — drop suppressions when the code underneath them changes.\n\n### Effect events: use `use-effect-event`, not React's native hook\n\nImport `useEffectEvent` from `use-effect-event`, never from `react`. On React 19.2 the native hook\nreturns first-render values when the calling component is wrapped in `forwardRef` or `memo`\n([facebook/react#34818](https://github.com/facebook/react/issues/34818), fixed in 19.3 canaries).\n`eslint/no-restricted-imports` in `.oxlintrc.json` enforces this. The bug reaches any dependency that\nwraps the native hook, so check the implementation before trusting one — `react-rx` is safe on both\nv4 and v5 because `useObservableEvent` builds on the same `use-effect-event` ponyfill.\n\n### Refs: use `props.ref`, not `forwardRef`\n\nReact 19 passes `ref` as a regular prop. Do not use `forwardRef` — destructure `ref` from props\n(so it is not left in a `...rest` spread) and forward it like any other prop.\n`eslint/no-restricted-imports` bans importing `forwardRef` from `react`.\n\nPrefer a named function declaration over `const X = function …` / arrow wrappers:\n\n```ts\n// preferred\nexport function MyComponent(props: Props & RefAttributes<HTMLDivElement>) {\n  const {ref, ...rest} = props\n  return <div ref={ref} {...rest} />\n}\n\n// avoid\nexport const MyComponent = function MyComponent(props: …) { … }\nexport const MyComponent = (props: …) => { … }\n```\n\nWhen wrapping with `memo`, declare the component as a function first, then memoize:\n\n```ts\nfunction MyComponent(props: …) { … }\nexport const MyComponentMemo = memo(MyComponent)\n```\n\nFor typings, include `ref` on the props type: stop omitting `'ref'` from `HTMLProps` /\n`ComponentProps`, or intersect with `RefAttributes<T>`. Avoid `PropsWithRef` — in `@types/react`\n19 it is a deprecated identity alias and trips `typescript/no-deprecated`.\n\n## Testing\n\n### Unit Tests (Vitest)\n\n```bash\npnpm test                    # Run all tests\npnpm test -- --watch        # Watch mode\npnpm test -- -u             # Update snapshots\npnpm test -- --project=sanity  # Run specific project\n```\n\nTests require a build first because some tests use compiled output:\n\n```bash\npnpm build && pnpm test\n```\n\n#### Test Timeouts\n\nWhen a test needs a custom timeout, use the Vitest options object as the second argument (not the deprecated third-argument form). Prefer numeric separators for readability:\n\n```ts\n// Correct\ntest('my test', {timeout: 30_000}, async () => {\n  // ...\n})\n\n// Wrong — timeout as third argument (deprecated)\ntest('my test', async () => {\n  // ...\n}, 30000)\n```\n\n#### Testing components that suspend via `use()`\n\nTwo traps when unit testing a component or hook that suspends on a promise with React's `use()`\n(see `packages/sanity/src/presentation/__tests__/useMainDocumentPolyfill.test.tsx`):\n\n- **Mount inside an awaited async `act`.** `render`/`renderHook` wrap the mount in an internal\n  _synchronous_ `act`, and React refuses to resume work that suspended inside an unawaited `act`\n  scope — the suspended tree parks forever and `waitFor` times out. Wrap the mount yourself:\n  `await act(async () => { renderHook(...) })` (suppress `testing-library/no-unnecessary-act` on\n  that line; this is the exception the rule doesn't know about). A `Suspense` wrapper is also\n  required.\n- **Keep the `use()` call sequence stable across the replay.** After the promise settles, React\n  _replays_ the suspended render reusing the recorded hook state. If the awaited promise's side\n  effect flips the condition guarding a conditional `use()` (e.g. a polyfill import that installs\n  a global the condition checks), the replay skips the `use()` call, hook accounting breaks, and\n  React throws `Update hook called on initial render` as a recoverable error — which vitest can\n  catch as an unhandled error and fail the run. Once a load has started, keep calling `use()` on\n  the same cached promise on every render instead of re-checking the environment.\n\n#### Vanilla-extract in jsdom tests\n\nThe `sanity` and `@sanity/vision` jsdom suites import\n[`@vanilla-extract/css/disableRuntimeStyles`](https://vanilla-extract.style/documentation/test-environments/#disabling-runtime-styles)\n(`packages/sanity/test/setup/environment.ts`, and as a direct vitest `setupFiles` entry in\n`packages/@sanity/vision/vitest.config.mts`), so vanilla-extract skips injecting real stylesheets\ninto jsdom. Class name identifiers still resolve, but computed styles are not available.\n\nConventions that follow from this:\n\n- **Do not assert on vanilla-extract class names or computed styles in jsdom tests.** Assert on\n  `data-testid` attributes instead. Visual/style behavior belongs in the vitest browser mode\n  suite (`*.browser.test.tsx`, real Chromium/Firefox/WebKit) or the Playwright e2e tests, where\n  runtime styles stay enabled.\n- **Keep `vanillaExtractPlugin()` in the vitest configs.** The plugin's transform assigns file\n  scopes to `.css.ts` modules; without it any test that (transitively) imports a `.css.ts` file\n  throws \"Styles were unable to be assigned to a file\". `disableRuntimeStyles` only skips style\n  injection, not the transform.\n\n#### @sanity/ui overlays stay mounted when closed\n\nFrom `@sanity/ui` v4, Tooltip/Popover/Menu keep their content mounted via React `<Activity>`\nwhile closed (hidden with `display: none`). Consequences for tests:\n\n- Plain text / test-id queries can match **closed** overlay content. Prefer scoping to the\n  visible element under test (or assert visibility) instead of `getByText` / `getByTestId` on\n  the whole document.\n- In jsdom, asserting that closed content is hidden works (`expect(...).not.toBeVisible()`), but\n  selecting the **open** overlay by visibility does not. Runtime styles are disabled there, so\n  nothing overrides the `hidden` attribute `@sanity/ui` puts on an open popover, and\n  `getByRole` (which skips inaccessible nodes) finds neither the open nor the closed copy. Pick\n  the open one by the absence of the `display: none` that `<Activity>` applies to closed\n  overlays, rather than by index:\n\n  ```ts\n  const [openMenu] = getAllByDataUi(document.body, 'MenuButton__popover').filter(\n    (popover) => popover.style.display !== 'none',\n  )\n  const item = within(openMenu).getByRole('menuitem', {name: 'Discard version', hidden: true})\n  ```\n\n  Selecting with `getAllByText(...)[0]` also works, but silently depends on portal ordering.\n  Visibility-based selection belongs in the browser-mode suite, where real styles apply and\n  `checkVisibility()` is meaningful.\n\n- Test routers must include intent routes (`route.create('/', [route.intents('/intent')])`).\n  Reference item menus render `IntentLink` (\"Open in new tab\") even while closed; without\n  intent routes, `resolveIntentLink` throws during render and the form subtree disappears.\n  See `packages/sanity/test/browser/TestWrapper.tsx` and `test/testUtils/TestProvider.tsx`.\n\n### Visual Regression Tests (Chromatic + Storybook)\n\nVisual regression runs on Chromatic via `.github/workflows/chromatic.yml`. `dev/storybook`\ncontains the stories — most reuse the vitest browser-mode test harnesses (`TestWrapper` +\n`*Story.tsx` components), plus authored migration sentinels for `ui-components` and\nvanilla-extract-migrated components.\n\n```bash\npnpm dev:storybook                    # Storybook dev server at http://localhost:6006\npnpm build:storybook                  # Static build via turbo (dev/storybook/storybook-static)\npnpm --filter sanity-storybook test   # Run every story as a vitest browser-mode test\nCHROMATIC=1 pnpm --filter sanity test:browser   # Chromatic archive capture run (chromium only)\n```\n\nRepo secrets: `CHROMATIC_PROJECT_TOKEN_STORYBOOK` (active), `CHROMATIC_PROJECT_TOKEN_E2E`\n(active, used by e2e), `CHROMATIC_PROJECT_TOKEN_VITEST` (dormant until Chromatic's Vitest early\naccess is enabled — the CI job self-activates when the secret is added). Checks are non-gating\nduring burn-in. See the `sanity-visual-regression` skill\n(`.agents/skills/sanity-visual-regression/SKILL.md`) for how to add coverage, determinism rules,\nand the Vitest activation runbook.\n\n### E2E Tests (Playwright)\n\n```bash\npnpm e2e:build              # Build E2E studio\npnpm test:e2e               # Run E2E tests\npnpm test:e2e --ui          # Interactive mode\n```\n\n## Pre-commit Hook\n\nLefthook runs on commit (see `lefthook.yml`), which:\n\n1. Runs oxfmt on staged files\n2. Runs oxlint `--fix` on staged `.js/.ts/.tsx` files (with `--no-error-on-unmatched-pattern` so packages in oxlint `ignorePatterns`, e.g. `@repo/test-dts-exports`, can still be committed)\n\nIf the hook fails, run `pnpm lint:fix` to fix issues.\n\n## Common Tasks\n\n### Adding a New Dependency\n\n```bash\n# Add to specific package\npnpm --filter sanity add <package>\n\n# Add to root (dev dependency)\npnpm add -w -D <package>\n```\n\n### Creating a New Test\n\n1. Create test file next to source: `MyComponent.test.tsx`\n2. Use existing test patterns from similar files\n3. Run `pnpm test -- MyComponent` to verify\n\n### Updating Snapshots\n\nWhen making intentional changes that affect snapshots:\n\n```bash\n# Update all snapshots\npnpm test -- -u\n\n# Update specific test's snapshots\npnpm test -- -u MyComponent\n```\n\nReview snapshot changes carefully before committing.\n\n## Commit Message Format and PR Title (CRITICAL)\n\nThis repo uses **conventional commits** for automated releases.\n\n**PR titles are validated by CI** using the [semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) action. A PR with a non-conforming title **will fail CI**.\n\n### Format\n\n```\ntype(scope): lowercase description without special characters\n```\n\n### Rules\n\n1. **Type** is required and must be one of: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `ci`\n2. **Scope** is required and should be the package or area affected (e.g., `groq`, `cli`, `form`, `deps`)\n3. **Description** must start with a lowercase letter\n4. **No backticks, quotes, or markdown** in the PR title — keep it plain text\n5. Use `fix` for bug fixes, `feat` for new features, `chore` for maintenance tasks\n\n### Choosing the Right Type\n\n- **`fix`** — Fixes a bug or resolves an issue (e.g., `fix(groq): resolve CJS type export issue`)\n- **`feat`** — Adds new functionality (e.g., `feat(form): add array input component`)\n- **`chore`** — Maintenance, dependency updates, CI changes (e.g., `chore(deps): update dependencies`)\n- **`docs`** — Documentation only (e.g., `docs(readme): improve installation instructions`)\n- **`refactor`** — Code restructuring without behavior change (e.g., `refactor(store): simplify document subscription logic`)\n- **`test`** — Adding or updating tests (e.g., `test(validation): add edge case coverage`)\n- **`perf`** — Performance improvements (e.g., `perf(search): optimize query execution`)\n- **`ci`** — CI/CD changes (e.g., `ci(e2e): add retry logic to flaky tests`)\n\n### Examples\n\n```\n# ✅ Good PR titles\nfix(groq): resolve CJS type export issue\nfeat(form): add new array input component\nchore(deps): update dependencies\n\n# ❌ Bad PR titles\nfeat(groq): add `types` condition     # no backticks allowed\nFix(cli): Handle missing config        # type must be lowercase, description must start lowercase\nadded new feature                       # missing type and scope\n```\n\n## Pull Request Workflow\n\n### 1. Create as Draft PR First\n\n**Always create PRs as drafts first.** The prompter (person who requested the work) reviews before the broader team.\n\n```bash\n# Create a draft PR — title MUST follow conventional commit format\ngh pr create --draft --title \"fix(scope): description\" --body \"...\" --label \"🤖 bot\"\n```\n\n### 2. Apply the \"🤖 bot\" Label\n\n**All PRs created by AI agents must be labeled with `🤖 bot`.** This label already exists on the repo and helps the team identify agent-created PRs for tracking and review workflows.\n\nWhen creating or updating a PR, always ensure the label is applied. If the create command did not accept `--label`, add it afterward:\n\n```bash\ngh pr edit --add-label \"🤖 bot\"\n```\n\n### 3. Move Out of Draft\n\nOnce the prompter approves and CI is green, convert from draft to ready-for-review:\n\n```bash\ngh pr ready\n```\n\n### 4. What Not To Touch Unless Asked\n\n- **`.github/CODEOWNERS`** — do not add or change ownership rules unless explicitly requested\n- **Release automation / version bumps** — versioning is driven by conventional commits on merge; do not open manual version PRs unless asked\n\n### Useful PR Labels\n\n| Label                | When to use                                                                          |\n| -------------------- | ------------------------------------------------------------------------------------ |\n| `🤖 bot`             | **Required** on every AI-agent PR                                                    |\n| `trigger: preview`   | Publishes preview packages via [`pkg.pr.new`](https://pkg.pr.new) (maintainer-gated) |\n| `trigger:perf-bench` | Runs the `perf/bench` suite on the PR (maintainer-gated)                             |\n| `full-test-suite`    | Forces the full unit test suite to run                                               |\n\nDo **not** apply `trigger:*` labels unless the prompter or a maintainer asks — they kick off expensive or publish workflows.\n\n### Crediting Original Authors (Ported / Cherry-picked Work)\n\nWhen porting or rebasing someone else's PR (community contribution, backport, etc.), credit the **original author**, not only the agent or whoever opens the port PR:\n\n1. Prefer commits authored as the original contributor when history allows:\n\n   ```bash\n   git commit --author=\"their-name <their-github-email>\" -m \"...\"\n   ```\n\n2. Otherwise add a `Co-authored-by:` trailer (and mention them in the PR description / Notes for release):\n\n   ```\n   Co-authored-by: Their Name <their-github-noreply@users.noreply.github.com>\n   ```\n\nWorkflow summary:\n\n1. **Agent creates draft PR** with the `🤖 bot` label\n2. **Prompter reviews** the draft\n3. **Mark ready for review** once the prompter approves\n4. **Team reviews** and merges\n\nThis ensures the person who prompted the changes can verify correctness before involving the broader team.\n\n## Keeping This Guide Updated\n\n**If you're asked to do something not documented here, update this file.**\n\nWhen working on a PR and you're asked to:\n\n- Run a command that isn't in this guide\n- Follow a workflow that isn't documented\n- Fix something using a non-obvious process\n\nAdd that knowledge to this `AGENTS.md` file as part of the same PR. This keeps the guide accurate and helps future agents (and humans) avoid the same gaps.\n\nExample: If asked \"run the e2e tests for just the form inputs\", and that's not documented, add it to the Testing section before completing the task.\n\n## Troubleshooting\n\n### Build Issues\n\n```bash\n# Clean everything and rebuild\npnpm clean && pnpm install && pnpm build\n```\n\n### Test Failures\n\n1. Ensure you've built: `pnpm build`\n2. Check if snapshots need updating: `pnpm test -- -u`\n3. Run specific test for better output: `pnpm test -- <test-name>`\n\n### Lint Failures\n\n```bash\n# Fix all lint issues\npnpm lint:fix\n\n# Check what would be fixed (dry run)\npnpm check:format\npnpm check:oxlint\n```\n\n## Environment Variables\n\nKey env vars used in development:\n\n- `SANITY_STUDIO_PROJECT_ID` - Project ID for dev studio\n- `SANITY_STUDIO_DATASET` - Dataset for dev studio\n- `SANITY_INTERNAL_ENV` - Internal environment flag\n\nSee `turbo.json` for full list of environment variables that affect builds.\n\n## Useful Links\n\n- [CONTRIBUTING.md](./CONTRIBUTING.md) - Full contribution guidelines\n- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) - Community guidelines\n- [packages/sanity/README.md](./packages/sanity/README.md) - Main package docs\n\n## Cursor Cloud specific instructions\n\nThese notes cover non-obvious gotchas for running in the Cursor Cloud VM. The startup update script already runs `pnpm install`.\n\n### Services\n\n| Service                                           | Port | Purpose                                          |\n| ------------------------------------------------- | ---- | ------------------------------------------------ |\n| Test studio (`pnpm dev` / `pnpm dev:test-studio`) | 3333 | Local Sanity Studio for manual verification      |\n| Preview iframe (`pnpm dev:preview-iframe`)        | 3334 | Cross-origin Presentation preview (vanilla Vite) |\n| Storybook (`pnpm dev:storybook`)                  | 6006 | Visual regression stories (Chromatic)            |\n\nNo Docker, databases, or other local services are required for unit tests, lint, or build. CI-style verification (`pnpm lint`, `pnpm build`, `pnpm test`) runs entirely in-process.\n\n### Gotchas\n\n- **Root `typescript` is TypeScript 7.** Catalog `typescript` (^7) is a normal root dependency and provides the native `tsc` binary for vitest typecheck (`*.test-d.*`) and for tsdown `dts: {tsgo: true}` (packages also declare catalog `typescript`). CI type checking of application code is owned by oxlint (`options.typeCheck`). Tools that still need the TypeScript 6 compiler API keep that isolated: `@repo/typedoc` (typedoc) and `@repo/test-dts-exports` (ts-morph) depend on `typescript` aliased to `@typescript/typescript6`. The old symlink workaround for a missing root `tsc` is no longer needed.\n- **Dev studio auth for cloud agents — use the `STUDIO_AUTH_TOKEN` secret, not interactive login.** `pnpm dev` runs `sanity dev --no-auto-updates` (non-interactive, no upgrade prompt) and serves the app at `http://localhost:3333`. The test studio connects to Sanity Cloud (project `ppsg7ml5`); its default workspace is `/test`. Without auth the workspaces show \"Signed out\" / \"Choose login provider\". To authenticate, put the injected `STUDIO_AUTH_TOKEN` in the URL hash — Sanity consumes it on load and strips it from the address bar:\n  - Build the URL: `node -e \"console.log('http://localhost:3333/test#token=' + encodeURIComponent(process.env.STUDIO_AUTH_TOKEN))\"` (any workspace basePath works, e.g. `/test`).\n  - Because the Read tool redacts the token, you cannot paste the URL into browser instructions directly. A reliable trick is a tiny local HTTP server that reads `STUDIO_AUTH_TOKEN` from env and serves an HTML page doing `location.replace(<studio-url-with-token>)`, then point the browser at that server (keeps the secret out of prompts/screenshots). After load you land authenticated in the workspace and can create/publish documents (e.g. an `Author`).\n  - Most changes should still be verified with `pnpm build && pnpm test` (no auth needed); only use the studio for visual/manual verification.\n- **Seeding test documents for the `/test` workspace via API.** In local dev (non-staging), the `/test` workspace talks to the production API host, so `STUDIO_AUTH_TOKEN` works as a Bearer token against `https://ppsg7ml5.api.sanity.io/v2024-01-01/data/mutate/test` (it returns 401 \"Session not found\" on `api.sanity.work`). Caveat when testing history/review-changes features: documents created by raw API mutations (e.g. `createOrReplace` of a published id) do not produce publish events, so the Review changes inspector shows \"There are no changes\" / \"Same revision selected\". Instead, create only the draft (`drafts.<id>`) via the API, click Publish in the studio UI to create a real publish event, then edit fields in the form to create draft changes.\n- **Seeding releases for the `/test` workspace via API.** Releases and document versions are created through the actions endpoint (`POST https://ppsg7ml5.api.sanity.io/v2025-02-19/data/actions/test` with `{\"actions\": [...]}`, same Bearer token). Useful action types: `sanity.action.release.create`, `sanity.action.document.version.create` (pass `publishedId` plus a `document` with `_id: versions.<releaseId>.<publishedId>`), `sanity.action.document.version.unpublish`, `sanity.action.document.version.discard`, `sanity.action.release.archive`, `sanity.action.release.delete`. Note that a version created by the unpublish action alone is an empty tombstone carrying only `_system.delete: true` — to get a version with content, create the version first and then unpublish it. `/test` is a shared dataset, so archive and delete any release you seed once you are done.\n- **Node version:** the VM runs Node 22.x, which satisfies the repo engine range (`>=22.12`). A couple of internal tooling packages print a harmless `Unsupported engine` warning wanting Node `>=22.18`; it does not affect testing or running the studio. However, **`pnpm build` requires Node >= 22.18**: the packages build with `tsdown`, which loads its `tsdown.config.ts` through Node's native TypeScript support and fails on older Node 22.x (e.g. the VM default `v22.14.0`) with `Failed to import module \"unrun\"`. A new enough runtime is available via nvm: `export PATH=\"$HOME/.nvm/versions/node/v22.22.2/bin:$PATH\"`.\n- **`pnpm build` may dirty `packages/sanity/package.json`.** tsdown auto-generates the `inlinedDependencies` field on every build, and in this VM the computed set can differ from what is committed (e.g. `@sanity/sdk` and `zustand` get dropped) even on a clean checkout of `main`. That churn is an environment artifact, not part of your change — revert it with `git checkout -- packages/sanity/package.json` (re-applying any edits of your own) instead of committing it.\n- **Do not run oxlint type checking (`pnpm check:oxlint`) while the dev studio is running.** Both are memory-hungry and running them concurrently has exhausted the VM's memory and frozen it for hours (unkillable thrashing). Stop `sanity dev` first (Ctrl-C in its tmux session), run the checks, then restart the studio.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md - AI Agent Guidelines for Sanity Monorepo\n\nThis document helps AI agents work successfully with the Sanity monorepo.\n\n> **Self-Improvement:** If you discover undocumented requirements, commands, or workflows during your work (e.g., a reviewer asks you to run something not covered here), update this file on the same PR. Keep this guide accurate and helpful for future agents.\n\n## Prerequisites\n\n- **Node.js**: v24 or latest LTS\n- **Package Manager**: pnpm v10+ (exact version managed via `packageManager` field in package.json)\n\n## Quick Reference\n\n```bash\n# Install dependencies (pnpm ONLY - enforced)\npnpm install\n\n# Build all packages (required before testing)\npnpm build\n\n# Run dev studio (requires auth, see below)\npnpm dev\n\n# Format code (MUST pass CI)\npnpm chore:format:fix\n\n# Fix all lint issues (MUST pass CI) — includes TypeScript type checking via oxlint\npnpm lint:fix\n\n# Run tests\npnpm test\n\n# Update snapshots if tests fail due to expected changes\npnpm test -- -u\n\n# Lint + type check (oxlint typeAware + typeCheck; no separate tsc step)\npnpm check:oxlint\n```\n\n## CI Checks - What Must Pass\n\nThese checks run on every PR and **must pass**:\n\n| Check            | Command               | Notes                                                                                                                                                            |\n| ---------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Format**       | `pnpm check:format`   | Uses oxfmt. Fix with `pnpm chore:format:fix`                                                                                                                     |\n| **Oxlint**       | `pnpm check:oxlint`   | Rust linter with type-aware rules and TypeScript type checking via tsgolint (`options.typeCheck`). Fix with `pnpm chore:oxlint:fix`                              |\n| **Unit Tests**   | `pnpm test`           | Vitest, sharded in CI                                                                                                                                            |\n| **Export Tests** | `pnpm test:exports`   | Ensures ESM/CJS/DTS work                                                                                                                                         |\n| **Dep Check**    | `pnpm depcheck`       | Finds unused/missing deps                                                                                                                                        |\n| **Zizmor**       | `pnpm lint:workflows` | Audits `.github/workflows/` for security issues. Fails CI on high-severity findings. Local run needs [`zizmor`](https://docs.zizmor.sh/installation/) on `PATH`. |\n| **PR Title**     | Conventional commits  | e.g., `feat(scope): description`                                                                                                                                 |\n\n### Before Committing\n\nRun these commands to avoid CI failures:\n\n```bash\n# Fix all formatting and lint issues\npnpm lint:fix\n\n# Verify tests pass (build first if needed)\npnpm build && pnpm test\n```\n\nIf tests fail due to **expected snapshot changes**, update them:\n\n```bash\npnpm test -- -u\n```\n\nSnapshot files are located in `__snapshots__` directories alongside test files.\n\n## Project Structure\n\n```\nsanity/\n├── packages/\n│   ├── sanity/           # Main Sanity studio package\n│   ├── @sanity/          # Scoped packages (cli, types, schema, etc.)\n│   └── @repo/            # Internal tooling (test-config, tsconfig, etc.)\n├── dev/                  # Development studios for testing\n│   ├── test-studio/      # Primary dev studio (pnpm dev runs this)\n│   └── preview-iframe/   # Presentation preview iframe (vanilla Vite, port 3334)\n├── e2e/                  # End-to-end Playwright tests\n├── perf/                 # Performance testing\n└── examples/             # Example studios\n```\n\n### Key Packages\n\n- **`packages/sanity`** - Core studio package with all UI components\n- **`packages/@sanity/types`** - TypeScript type definitions\n- **`packages/@sanity/schema`** - Schema compilation\n- **`packages/@sanity/mutator`** - Document mutation logic\n\n## Build System\n\n- **Package Manager**: pnpm (version 10.x, enforced via `preinstall`)\n- **Build Orchestration**: Turbo (caches builds)\n- **Versioning**: Lerna-lite with conventional commits\n\n### Build Commands\n\n```bash\npnpm build              # Build all packages\npnpm watch              # Watch mode for development\n```\n\n### Running the Dev Studio\n\n```bash\npnpm dev                # Starts dev studio at http://localhost:3333\n```\n\n**Note:** The dev studio requires Sanity user authentication in the browser. It's a Vite application that communicates with Sanity API endpoints, so you'll need to log in with a Sanity account when you access `http://localhost:3333` to use the studio.\n\n## Local Development\n\nThis section clarifies what requires authentication and what doesn't—critical for AI agents to avoid getting stuck on auth flows.\n\n### Running Tests (No Auth Required)\n\nUnit tests run in jsdom with mocks and **do not require any authentication**:\n\n```bash\n# Build first (required), then run all tests\npnpm build && pnpm test\n\n# Run a single test file (IMPORTANT: use vitest directly with --project to avoid running all tests)\npnpm vitest run --project=sanity packages/sanity/src/core/hooks/useClient.test.ts\n\n# Run a single test file with verbose output\npnpm vitest run --project=sanity --reporter=verbose packages/sanity/src/core/hooks/useClient.test.ts\n\n# Watch mode for iterative development\npnpm test -- --watch\n\n# Run tests for a specific package\npnpm test -- --project=sanity\n```\n\n**Important:** Do NOT use `pnpm test -- path/to/file.test.ts` for running a single file — it runs all tests across all projects. Use `pnpm vitest run --project=<project> <path>` instead.\n\nComponents that need auth context use `createMockAuthStore` in tests, so no real authentication is needed. This is the recommended way to verify most code changes.\n\n### Running the Dev Studio (Auth Required)\n\n```bash\npnpm dev  # Starts test-studio at http://localhost:3333 and preview-iframe at http://localhost:3334\n```\n\n- **Requires browser authentication** on first visit—you'll be prompted to log in with a Sanity account\n- Connects to a real Sanity project (configured in `dev/test-studio/sanity.config.ts`)\n- Uses staging API by default (`api.sanity.work`)\n- Session persists in browser, so subsequent visits won't require re-authentication\n- `pnpm dev` / `pnpm dev:test-studio` also starts `dev/preview-iframe` (vanilla Vite on port 3334) so Presentation can load its cross-origin iframe. Studio-only: `pnpm dev:test-studio:studio`. Preview-only: `pnpm dev:preview-iframe`.\n- Deployed preview iframe: Sanity Sandbox Vercel project `test-studio-preview-iframe` (`https://test-studio-preview-iframe.sanity.dev`)\n\nUse the dev studio when you need to:\n\n- Visually verify UI changes\n- Test real document editing workflows\n- Debug issues that only appear with real data\n- Exercise Presentation / visual editing against the local preview iframe\n\n### Inspecting Production Builds with Vite DevTools\n\nThe test studio can run with [Vite DevTools](https://devtools.vite.dev) enabled, which lets you inspect the output of `sanity build` runs (module graph, chunks, plugin timings, bundle treemaps, session diffing) from inside a long-running `sanity dev` server—no restart needed.\n\n```bash\n# Builds the test studio with devtools enabled, then starts the dev server\n# (so there's a build session to inspect right away)\npnpm devtools:test-studio\n```\n\nOpen `http://localhost:3333` and use the Vite DevTools dock to explore the recorded Rolldown build session. See the [DevTools for Rolldown features guide](https://devtools.vite.dev/rolldown/features.html) for how to use the module graph, chunk, asset, and plugin panels.\n\nTo inspect a **new** build after making changes—while `pnpm devtools:test-studio` is still running—run in a second terminal:\n\n```bash\n# Creates a fresh build session that shows up in the running DevTools dock\npnpm devtools:test-studio:build\n```\n\nBuilds are not hooked into HMR; `sanity build` must be invoked manually (via the command above) each time you want a new session to inspect. Sessions can be compared against each other in the DevTools UI to diff bundle changes.\n\nHow it works:\n\n- Both commands set `ENABLE_VITE_DEVTOOLS=true`, which makes `dev/test-studio/sanity.cli.ts` add the `DevTools()` Vite plugin and enable `build.rolldownOptions.devtools`\n- Build sessions are written to `dev/test-studio/node_modules/.rolldown` (gitignored)\n- The flag is declared in `dev/test-studio/turbo.json` so turbo-cached builds are invalidated when it changes\n- Enabling devtools makes `sanity build` noticeably slower; that's why it's opt-in via the env flag\n\n### Studio performance benchmarks (perf/bench — No Auth Required)\n\nThe `perf/bench` suite benchmarks a built studio against a **local mock** of the Sanity API — fully hermetic, no tokens, no network:\n\n```bash\npnpm build:bench                                   # build packages + bench studio (required first)\npnpm bench help                                    # list all bench CLI commands\npnpm bench run --scenario singleString             # absolute interaction benchmark\npnpm bench run --mode pageload --scenario singleString  # load vitals + bundle size\npnpm bench:unit                                    # mock-contract + stats unit tests\npnpm bench dev                                     # mock + `sanity dev` for interactive debugging\n```\n\nSee `perf/bench/README.md` for A/B comparisons, scenarios, and CI details. `dev/efps` is the legacy perf suite, kept for reference while perf/bench burns in.\n\n### E2E Tests (Token Required)\n\nE2E tests require authentication tokens. Add these to `.env.local` in the repo root:\n\n```bash\nSANITY_E2E_SESSION_TOKEN=<your-token>\nSANITY_E2E_PROJECT_ID=<project-id>\nSANITY_E2E_DATASET=<dataset-name>\n```\n\n**How to get a token:**\n\n```bash\n# Option 1: Use your CLI token\nsanity login\nsanity debug --secrets  # Look for \"Auth token\"\n\n# Option 2: Create a project token at https://sanity.io/manage\n# Navigate to: Project Settings → API → Tokens → Add API token\n```\n\nThen run E2E tests:\n\n```bash\npnpm e2e:build              # Build E2E studio\npnpm test:e2e               # Run E2E tests\npnpm test:e2e --ui          # Interactive mode\n```\n\n**Note:** E2E tests are typically run in CI, not locally during development. Most changes can be verified with unit tests.\n\n### Important Note for AI Agents\n\n**What requires authentication:**\n\n- Running the dev studio (`pnpm dev`)\n- E2E tests (`pnpm test:e2e`)\n- Any command that connects to Sanity APIs\n\n**What does NOT require authentication:**\n\n- Building packages (`pnpm build`)\n- Running unit tests (`pnpm test`)\n- Linting, formatting, and type checking (`pnpm lint`, `pnpm lint:fix`, `pnpm check:oxlint`)\n\n**Recommendation:** For most code changes, use `pnpm build && pnpm test` to verify correctness. This covers the vast majority of development tasks without any auth setup. Only use the dev studio when visual verification is specifically needed.\n\n## Coding Standards\n\nCoding standards are enforced by **oxlint** (native Rust rules, type-aware rules via tsgolint, TypeScript type checking via `options.typeCheck`, and a few ESLint plugins loaded through oxlint's `jsPlugins`). TypeScript type checking is included in `pnpm lint` / `pnpm check:oxlint` — no separate `tsc` step. Check your code with:\n\n```bash\npnpm lint              # Check for issues (oxlint, includes type checking)\npnpm lint:fix          # Auto-fix issues (oxfmt + oxlint --fix)\n```\n\nAll packages use **ESM** (`\"type\": \"module\"`). TypeScript strict mode is enabled.\n\nRules that the linter already enforces (restricted imports, type-aware rules, React Compiler rules, i18n rules, module boundaries) are not repeated in this guide — run `pnpm lint` and follow the reported messages, which explain the expected pattern.\n\n### Do Not Weaken the Linter\n\nFix the reported problem instead of silencing it. In order of preference:\n\n1. **Fix the code** so the rule passes. This is almost always the right answer.\n2. **Suppress the single line** as a last resort, when the rule is genuinely wrong for that one spot: `// oxlint-disable-next-line <rule> -- <why>`. Always name the specific rule and explain the exception after `--`. Never suppress a rule merely to make CI green.\n3. **Change `.oxlintrc.json` only when a human explicitly asks.** Do not turn rules off, downgrade severity, add `overrides` entries, or widen `ignorePatterns` on your own initiative — an override silences the rule for every current and future file it matches. If you think a rule is wrong, leave it failing and raise it in your summary or the PR description.\n\nFile-wide `/* oxlint-disable <rule> */` is reserved for files that are an exception as a whole — vendored code, the `packages/sanity/src/ui-components` wrappers around raw `@sanity/ui`, CLI scripts that print to `console`. Follow that existing precedent rather than reaching for it to clear a handful of errors.\n\n`options.reportUnusedDisableDirectives` is `error`, so a suppression that stops being necessary fails CI — drop suppressions when the code underneath them changes.\n\n### Effect events: use `use-effect-event`, not React's native hook\n\nImport `useEffectEvent` from `use-effect-event`, never from `react`. On React 19.2 the native hook\nreturns first-render values when the calling component is wrapped in `forwardRef` or `memo`\n([facebook/react#34818](https://github.com/facebook/react/issues/34818), fixed in 19.3 canaries).\n`eslint/no-restricted-imports` in `.oxlintrc.json` enforces this. The bug reaches any dependency that\nwraps the native hook, so check the implementation before trusting one — `react-rx` is safe on both\nv4 and v5 because `useObservableEvent` builds on the same `use-effect-event` ponyfill.\n\n### Refs: use `props.ref`, not `forwardRef`\n\nReact 19 passes `ref` as a regular prop. Do not use `forwardRef` — destructure `ref` from props\n(so it is not left in a `...rest` spread) and forward it like any other prop.\n`eslint/no-restricted-imports` bans importing `forwardRef` from `react`.\n\nPrefer a named function declaration over `const X = function …` / arrow wrappers:\n\n```ts\n// preferred\nexport function MyComponent(props: Props & RefAttributes<HTMLDivElement>) {\n  const {ref, ...rest} = props\n  return <div ref={ref} {...rest} />\n}\n\n// avoid\nexport const MyComponent = function MyComponent(props: …) { … }\nexport const MyComponent = (props: …) => { … }\n```\n\nWhen wrapping with `memo`, declare the component as a function first, then memoize:\n\n```ts\nfunction MyComponent(props: …) { … }\nexport const MyComponentMemo = memo(MyComponent)\n```\n\nFor typings, include `ref` on the props type: stop omitting `'ref'` from `HTMLProps` /\n`ComponentProps`, or intersect with `RefAttributes<T>`. Avoid `PropsWithRef` — in `@types/react`\n19 it is a deprecated identity alias and trips `typescript/no-deprecated`.\n\n## Testing\n\n### Unit Tests (Vitest)\n\n```bash\npnpm test                    # Run all tests\npnpm test -- --watch        # Watch mode\npnpm test -- -u             # Update snapshots\npnpm test -- --project=sanity  # Run specific project\n```\n\nTests require a build first because some tests use compiled output:\n\n```bash\npnpm build && pnpm test\n```\n\n#### Test Timeouts\n\nWhen a test needs a custom timeout, use the Vitest options object as the second argument (not the deprecated third-argument form). Prefer numeric separators for readability:\n\n```ts\n// Correct\ntest('my test', {timeout: 30_000}, async () => {\n  // ...\n})\n\n// Wrong — timeout as third argument (deprecated)\ntest('my test', async () => {\n  // ...\n}, 30000)\n```\n\n#### Testing components that suspend via `use()`\n\nTwo traps when unit testing a component or hook that suspends on a promise with React's `use()`\n(see `packages/sanity/src/presentation/__tests__/useMainDocumentPolyfill.test.tsx`):\n\n- **Mount inside an awaited async `act`.** `render`/`renderHook` wrap the mount in an internal\n  _synchronous_ `act`, and React refuses to resume work that suspended inside an unawaited `act`\n  scope — the suspended tree parks forever and `waitFor` times out. Wrap the mount yourself:\n  `await act(async () => { renderHook(...) })` (suppress `testing-library/no-unnecessary-act` on\n  that line; this is the exception the rule doesn't know about). A `Suspense` wrapper is also\n  required.\n- **Keep the `use()` call sequence stable across the replay.** After the promise settles, React\n  _replays_ the suspended render reusing the recorded hook state. If the awaited promise's side\n  effect flips the condition guarding a conditional `use()` (e.g. a polyfill import that installs\n  a global the condition checks), the replay skips the `use()` call, hook accounting breaks, and\n  React throws `Update hook called on initial render` as a recoverable error — which vitest can\n  catch as an unhandled error and fail the run. Once a load has started, keep calling `use()` on\n  the same cached promise on every render instead of re-checking the environment.\n\n#### Vanilla-extract in jsdom tests\n\nThe `sanity` and `@sanity/vision` jsdom suites import\n[`@vanilla-extract/css/disableRuntimeStyles`](https://vanilla-extract.style/documentation/test-environments/#disabling-runtime-styles)\n(`packages/sanity/test/setup/environment.ts`, and as a direct vitest `setupFiles` entry in\n`packages/@sanity/vision/vitest.config.mts`), so vanilla-extract skips injecting real stylesheets\ninto jsdom. Class name identifiers still resolve, but computed styles are not available.\n\nConventions that follow from this:\n\n- **Do not assert on vanilla-extract class names or computed styles in jsdom tests.** Assert on\n  `data-testid` attributes instead. Visual/style behavior belongs in the vitest browser mode\n  suite (`*.browser.test.tsx`, real Chromium/Firefox/WebKit) or the Playwright e2e tests, where\n  runtime styles stay enabled.\n- **Keep `vanillaExtractPlugin()` in the vitest configs.** The plugin's transform assigns file\n  scopes to `.css.ts` modules; without it any test that (transitively) imports a `.css.ts` file\n  throws \"Styles were unable to be assigned to a file\". `disableRuntimeStyles` only skips style\n  injection, not the transform.\n\n#### @sanity/ui overlays stay mounted when closed\n\nFrom `@sanity/ui` v4, Tooltip/Popover/Menu keep their content mounted via React `<Activity>`\nwhile closed (hidden with `display: none`). Consequences for tests:\n\n- Plain text / test-id queries can match **closed** overlay content. Prefer scoping to the\n  visible element under test (or assert visibility) instead of `getByText` / `getByTestId` on\n  the whole document.\n- In jsdom, asserting that closed content is hidden works (`expect(...).not.toBeVisible()`), but\n  selecting the **open** overlay by visibility does not. Runtime styles are disabled there, so\n  nothing overrides the `hidden` attribute `@sanity/ui` puts on an open popover, and\n  `getByRole` (which skips inaccessible nodes) finds neither the open nor the closed copy. Pick\n  the open one by the absence of the `display: none` that `<Activity>` applies to closed\n  overlays, rather than by index:\n\n  ```ts\n  const [openMenu] = getAllByDataUi(document.body, 'MenuButton__popover').filter(\n    (popover) => popover.style.display !== 'none',\n  )\n  const item = within(openMenu).getByRole('menuitem', {name: 'Discard version', hidden: true})\n  ```\n\n  Selecting with `getAllByText(...)[0]` also works, but silently depends on portal ordering.\n  Visibility-based selection belongs in the browser-mode suite, where real styles apply and\n  `checkVisibility()` is meaningful.\n\n- Test routers must include intent routes (`route.create('/', [route.intents('/intent')])`).\n  Reference item menus render `IntentLink` (\"Open in new tab\") even while closed; without\n  intent routes, `resolveIntentLink` throws during render and the form subtree disappears.\n  See `packages/sanity/test/browser/TestWrapper.tsx` and `test/testUtils/TestProvider.tsx`.\n\n### Visual Regression Tests (Chromatic + Storybook)\n\nVisual regression runs on Chromatic via `.github/workflows/chromatic.yml`. `dev/storybook`\ncontains the stories — most reuse the vitest browser-mode test harnesses (`TestWrapper` +\n`*Story.tsx` components), plus authored migration sentinels for `ui-components` and\nvanilla-extract-migrated components.\n\n```bash\npnpm dev:storybook                    # Storybook dev server at http://localhost:6006\npnpm build:storybook                  # Static build via turbo (dev/storybook/storybook-static)\npnpm --filter sanity-storybook test   # Run every story as a vitest browser-mode test\nCHROMATIC=1 pnpm --filter sanity test:browser   # Chromatic archive capture run (chromium only)\n```\n\nRepo secrets: `CHROMATIC_PROJECT_TOKEN_STORYBOOK` (active), `CHROMATIC_PROJECT_TOKEN_E2E`\n(active, used by e2e), `CHROMATIC_PROJECT_TOKEN_VITEST` (dormant until Chromatic's Vitest early\naccess is enabled — the CI job self-activates when the secret is added). Checks are non-gating\nduring burn-in. See the `sanity-visual-regression` skill\n(`.agents/skills/sanity-visual-regression/SKILL.md`) for how to add coverage, determinism rules,\nand the Vitest activation runbook.\n\n### E2E Tests (Playwright)\n\n```bash\npnpm e2e:build              # Build E2E studio\npnpm test:e2e               # Run E2E tests\npnpm test:e2e --ui          # Interactive mode\n```\n\n## Pre-commit Hook\n\nLefthook runs on commit (see `lefthook.yml`), which:\n\n1. Runs oxfmt on staged files\n2. Runs oxlint `--fix` on staged `.js/.ts/.tsx` files (with `--no-error-on-unmatched-pattern` so packages in oxlint `ignorePatterns`, e.g. `@repo/test-dts-exports`, can still be committed)\n\nIf the hook fails, run `pnpm lint:fix` to fix issues.\n\n## Common Tasks\n\n### Adding a New Dependency\n\n```bash\n# Add to specific package\npnpm --filter sanity add <package>\n\n# Add to root (dev dependency)\npnpm add -w -D <package>\n```\n\n### Creating a New Test\n\n1. Create test file next to source: `MyComponent.test.tsx`\n2. Use existing test patterns from similar files\n3. Run `pnpm test -- MyComponent` to verify\n\n### Updating Snapshots\n\nWhen making intentional changes that affect snapshots:\n\n```bash\n# Update all snapshots\npnpm test -- -u\n\n# Update specific test's snapshots\npnpm test -- -u MyComponent\n```\n\nReview snapshot changes carefully before committing.\n\n## Commit Message Format and PR Title (CRITICAL)\n\nThis repo uses **conventional commits** for automated releases.\n\n**PR titles are validated by CI** using the [semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) action. A PR with a non-conforming title **will fail CI**.\n\n### Format\n\n```\ntype(scope): lowercase description without special characters\n```\n\n### Rules\n\n1. **Type** is required and must be one of: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `ci`\n2. **Scope** is required and should be the package or area affected (e.g., `groq`, `cli`, `form`, `deps`)\n3. **Description** must start with a lowercase letter\n4. **No backticks, quotes, or markdown** in the PR title — keep it plain text\n5. Use `fix` for bug fixes, `feat` for new features, `chore` for maintenance tasks\n\n### Choosing the Right Type\n\n- **`fix`** — Fixes a bug or resolves an issue (e.g., `fix(groq): resolve CJS type export issue`)\n- **`feat`** — Adds new functionality (e.g., `feat(form): add array input component`)\n- **`chore`** — Maintenance, dependency updates, CI changes (e.g., `chore(deps): update dependencies`)\n- **`docs`** — Documentation only (e.g., `docs(readme): improve installation instructions`)\n- **`refactor`** — Code restructuring without behavior change (e.g., `refactor(store): simplify document subscription logic`)\n- **`test`** — Adding or updating tests (e.g., `test(validation): add edge case coverage`)\n- **`perf`** — Performance improvements (e.g., `perf(search): optimize query execution`)\n- **`ci`** — CI/CD changes (e.g., `ci(e2e): add retry logic to flaky tests`)\n\n### Examples\n\n```\n# ✅ Good PR titles\nfix(groq): resolve CJS type export issue\nfeat(form): add new array input component\nchore(deps): update dependencies\n\n# ❌ Bad PR titles\nfeat(groq): add `types` condition     # no backticks allowed\nFix(cli): Handle missing config        # type must be lowercase, description must start lowercase\nadded new feature                       # missing type and scope\n```\n\n## Pull Request Workflow\n\n### 1. Create as Draft PR First\n\n**Always create PRs as drafts first.** The prompter (person who requested the work) reviews before the broader team.\n\n```bash\n# Create a draft PR — title MUST follow conventional commit format\ngh pr create --draft --title \"fix(scope): description\" --body \"...\" --label \"🤖 bot\"\n```\n\n### 2. Apply the \"🤖 bot\" Label\n\n**All PRs created by AI agents must be labeled with `🤖 bot`.** This label already exists on the repo and helps the team identify agent-created PRs for tracking and review workflows.\n\nWhen creating or updating a PR, always ensure the label is applied. If the create command did not accept `--label`, add it afterward:\n\n```bash\ngh pr edit --add-label \"🤖 bot\"\n```\n\n### 3. Move Out of Draft\n\nOnce the prompter approves and CI is green, convert from draft to ready-for-review:\n\n```bash\ngh pr ready\n```\n\n### 4. What Not To Touch Unless Asked\n\n- **`.github/CODEOWNERS`** — do not add or change ownership rules unless explicitly requested\n- **Release automation / version bumps** — versioning is driven by conventional commits on merge; do not open manual version PRs unless asked\n\n### Useful PR Labels\n\n| Label                | When to use                                                                          |\n| -------------------- | ------------------------------------------------------------------------------------ |\n| `🤖 bot`             | **Required** on every AI-agent PR                                                    |\n| `trigger: preview`   | Publishes preview packages via [`pkg.pr.new`](https://pkg.pr.new) (maintainer-gated) |\n| `trigger:perf-bench` | Runs the `perf/bench` suite on the PR (maintainer-gated)                             |\n| `full-test-suite`    | Forces the full unit test suite to run                                               |\n\nDo **not** apply `trigger:*` labels unless the prompter or a maintainer asks — they kick off expensive or publish workflows.\n\n### Crediting Original Authors (Ported / Cherry-picked Work)\n\nWhen porting or rebasing someone else's PR (community contribution, backport, etc.), credit the **original author**, not only the agent or whoever opens the port PR:\n\n1. Prefer commits authored as the original contributor when history allows:\n\n   ```bash\n   git commit --author=\"their-name <their-github-email>\" -m \"...\"\n   ```\n\n2. Otherwise add a `Co-authored-by:` trailer (and mention them in the PR description / Notes for release):\n\n   ```\n   Co-authored-by: Their Name <their-github-noreply@users.noreply.github.com>\n   ```\n\nWorkflow summary:\n\n1. **Agent creates draft PR** with the `🤖 bot` label\n2. **Prompter reviews** the draft\n3. **Mark ready for review** once the prompter approves\n4. **Team reviews** and merges\n\nThis ensures the person who prompted the changes can verify correctness before involving the broader team.\n\n## Keeping This Guide Updated\n\n**If you're asked to do something not documented here, update this file.**\n\nWhen working on a PR and you're asked to:\n\n- Run a command that isn't in this guide\n- Follow a workflow that isn't documented\n- Fix something using a non-obvious process\n\nAdd that knowledge to this `AGENTS.md` file as part of the same PR. This keeps the guide accurate and helps future agents (and humans) avoid the same gaps.\n\nExample: If asked \"run the e2e tests for just the form inputs\", and that's not documented, add it to the Testing section before completing the task.\n\n## Troubleshooting\n\n### Build Issues\n\n```bash\n# Clean everything and rebuild\npnpm clean && pnpm install && pnpm build\n```\n\n### Test Failures\n\n1. Ensure you've built: `pnpm build`\n2. Check if snapshots need updating: `pnpm test -- -u`\n3. Run specific test for better output: `pnpm test -- <test-name>`\n\n### Lint Failures\n\n```bash\n# Fix all lint issues\npnpm lint:fix\n\n# Check what would be fixed (dry run)\npnpm check:format\npnpm check:oxlint\n```\n\n## Environment Variables\n\nKey env vars used in development:\n\n- `SANITY_STUDIO_PROJECT_ID` - Project ID for dev studio\n- `SANITY_STUDIO_DATASET` - Dataset for dev studio\n- `SANITY_INTERNAL_ENV` - Internal environment flag\n\nSee `turbo.json` for full list of environment variables that affect builds.\n\n## Useful Links\n\n- [CONTRIBUTING.md](./CONTRIBUTING.md) - Full contribution guidelines\n- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) - Community guidelines\n- [packages/sanity/README.md](./packages/sanity/README.md) - Main package docs\n\n## Cursor Cloud specific instructions\n\nThese notes cover non-obvious gotchas for running in the Cursor Cloud VM. The startup update script already runs `pnpm install`.\n\n### Services\n\n| Service                                           | Port | Purpose                                          |\n| ------------------------------------------------- | ---- | ------------------------------------------------ |\n| Test studio (`pnpm dev` / `pnpm dev:test-studio`) | 3333 | Local Sanity Studio for manual verification      |\n| Preview iframe (`pnpm dev:preview-iframe`)        | 3334 | Cross-origin Presentation preview (vanilla Vite) |\n| Storybook (`pnpm dev:storybook`)                  | 6006 | Visual regression stories (Chromatic)            |\n\nNo Docker, databases, or other local services are required for unit tests, lint, or build. CI-style verification (`pnpm lint`, `pnpm build`, `pnpm test`) runs entirely in-process.\n\n### Gotchas\n\n- **Root `typescript` is TypeScript 7.** Catalog `typescript` (^7) is a normal root dependency and provides the native `tsc` binary for vitest typecheck (`*.test-d.*`) and for tsdown `dts: {tsgo: true}` (packages also declare catalog `typescript`). CI type checking of application code is owned by oxlint (`options.typeCheck`). Tools that still need the TypeScript 6 compiler API keep that isolated: `@repo/typedoc` (typedoc) and `@repo/test-dts-exports` (ts-morph) depend on `typescript` aliased to `@typescript/typescript6`. The old symlink workaround for a missing root `tsc` is no longer needed.\n- **Dev studio auth for cloud agents — use the `STUDIO_AUTH_TOKEN` secret, not interactive login.** `pnpm dev` runs `sanity dev --no-auto-updates` (non-interactive, no upgrade prompt) and serves the app at `http://localhost:3333`. The test studio connects to Sanity Cloud (project `ppsg7ml5`); its default workspace is `/test`. Without auth the workspaces show \"Signed out\" / \"Choose login provider\". To authenticate, put the injected `STUDIO_AUTH_TOKEN` in the URL hash — Sanity consumes it on load and strips it from the address bar:\n  - Build the URL: `node -e \"console.log('http://localhost:3333/test#token=' + encodeURIComponent(process.env.STUDIO_AUTH_TOKEN))\"` (any workspace basePath works, e.g. `/test`).\n  - Because the Read tool redacts the token, you cannot paste the URL into browser instructions directly. A reliable trick is a tiny local HTTP server that reads `STUDIO_AUTH_TOKEN` from env and serves an HTML page doing `location.replace(<studio-url-with-token>)`, then point the browser at that server (keeps the secret out of prompts/screenshots). After load you land authenticated in the workspace and can create/publish documents (e.g. an `Author`).\n  - Most changes should still be verified with `pnpm build && pnpm test` (no auth needed); only use the studio for visual/manual verification.\n- **Seeding test documents for the `/test` workspace via API.** In local dev (non-staging), the `/test` workspace talks to the production API host, so `STUDIO_AUTH_TOKEN` works as a Bearer token against `https://ppsg7ml5.api.sanity.io/v2024-01-01/data/mutate/test` (it returns 401 \"Session not found\" on `api.sanity.work`). Caveat when testing history/review-changes features: documents created by raw API mutations (e.g. `createOrReplace` of a published id) do not produce publish events, so the Review changes inspector shows \"There are no changes\" / \"Same revision selected\". Instead, create only the draft (`drafts.<id>`) via the API, click Publish in the studio UI to create a real publish event, then edit fields in the form to create draft changes.\n- **Seeding releases for the `/test` workspace via API.** Releases and document versions are created through the actions endpoint (`POST https://ppsg7ml5.api.sanity.io/v2025-02-19/data/actions/test` with `{\"actions\": [...]}`, same Bearer token). Useful action types: `sanity.action.release.create`, `sanity.action.document.version.create` (pass `publishedId` plus a `document` with `_id: versions.<releaseId>.<publishedId>`), `sanity.action.document.version.unpublish`, `sanity.action.document.version.discard`, `sanity.action.release.archive`, `sanity.action.release.delete`. Note that a version created by the unpublish action alone is an empty tombstone carrying only `_system.delete: true` — to get a version with content, create the version first and then unpublish it. `/test` is a shared dataset, so archive and delete any release you seed once you are done.\n- **Node version:** the VM runs Node 22.x, which satisfies the repo engine range (`>=22.12`). A couple of internal tooling packages print a harmless `Unsupported engine` warning wanting Node `>=22.18`; it does not affect testing or running the studio. However, **`pnpm build` requires Node >= 22.18**: the packages build with `tsdown`, which loads its `tsdown.config.ts` through Node's native TypeScript support and fails on older Node 22.x (e.g. the VM default `v22.14.0`) with `Failed to import module \"unrun\"`. A new enough runtime is available via nvm: `export PATH=\"$HOME/.nvm/versions/node/v22.22.2/bin:$PATH\"`.\n- **`pnpm build` may dirty `packages/sanity/package.json`.** tsdown auto-generates the `inlinedDependencies` field on every build, and in this VM the computed set can differ from what is committed (e.g. `@sanity/sdk` and `zustand` get dropped) even on a clean checkout of `main`. That churn is an environment artifact, not part of your change — revert it with `git checkout -- packages/sanity/package.json` (re-applying any edits of your own) instead of committing it.\n- **Do not run oxlint type checking (`pnpm check:oxlint`) while the dev studio is running.** Both are memory-hungry and running them concurrently has exhausted the VM's memory and frozen it for hours (unkillable thrashing). Stop `sanity dev` first (Ctrl-C in its tmux session), run the checks, then restart the studio.\n","category":"root","tokens":8714}]}