Files
orca/docs/site
Jinwoo Hong 90c1c0ac50 feat(mobile): draw the host Accounts screen from the desktop's page (#30020)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): delete native screens the OTA shell no longer reaches

With every build the OTA shell, these native siblings run nowhere: the
native-executed closure (index.js plus every non-web route) does not reach
them, and the page resolves their .web siblings. Each .web sibling takes the
plain name, its web-overrides entry goes, and the native-only rich Markdown
editor and Mermaid WebView generators and their outputs go with them.

Kept: native siblings the node test suite still renders shared screens
through (terminal WebView, browser frame path, clipboard, dictation, media
picker, live input, snapshot budget, input styles).

Claude-Session: https://claude.ai/code/session_6cea92f6

* test(mobile): drop the phone-preview comparison the deleted native sibling held

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps

MobileSelectableText was react-native's Text, so callers use Text. The rich
Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are
required now that the page is its only host; the WebView defaults go. Comments
that described deleted native components are rewritten, and
expo-image-manipulator and react-native-uitextview leave package.json and the
lockfile.

Claude-Session: https://claude.ai/code/session_6cea92f6

* feat(mobile): draw the host Accounts screen from the desktop's page

Accounts was the last host screen the shell still drew natively, so its
changes needed a phone build. It is now a page route like Tasks: the
screen moves to src/accounts/MobileAccountsScreen.tsx, accounts.web.tsx
renders it on the page, and accounts.tsx mounts the shell for it.

What React Native Web needed: Alert.alert is a no-op there, so the
reset confirmation, its outcome and a failed account switch are
ConfirmModal drawers (ConfirmModal can now drop Cancel for a notice);
Back goes through the route handoff, which pops the shell's stack; and
the reset idempotency key is built from getRandomBytes, since
randomUUID needs a secure context the iOS custom scheme is not.

edit.tsx edits the phone's own pairing and never renders on the page,
so the route manifest leaves it out and the page's host stack does not
declare it. The script sweep is re-measured for the 14-route tree.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): answer a same-offer Codex reset retry with its recorded outcome; mobile mirrors the desktop

The desktop keeps no client journal for a reset credit: the host's ledger
owns the attempt. A remote client that lost a reply and retried the same
offer under a new key was refused with "already attempted" (or "unknown
outcome" while the provider call was unresolved), which is why the phone
carried its own durable journal to replay the old key.

The coordinator now answers a request for an offer an earlier key already
claimed as that attempt: the settled outcome, the running attempt, or a
resume of the pending one under its original key. A different offer for
the account is still refused while an outcome is unknown, so nothing can
spend twice. The reply shape is the one a same-key replay always had, so
installed clients with their journal read it unchanged.

With that, the Accounts page sends a fresh key per attempt like the
desktop and keeps nothing on the phone: the AsyncStorage journal, its
client singleflight and the retry-record warnings are deleted, and the
journal-resume recording goes with them.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): resume an account's unresolved Codex reset when the retry names a newer offer

Review follow-ups:
- Host: after an unknown provider outcome the usage refresh moves the
  offer, so a retry names a newer one. The coordinator now answers it as
  the account's unresolved attempt (resumed under its own key and scope),
  as the desktop's consumeCurrent does, instead of refusing forever. One
  ledger lookup finds own key, offer claimant, then unresolved attempt.
- Mobile: a reply is matched on target, account and account revision,
  not the offer, so the resumed attempt's scope is accepted.
- The key is minted inside requestCodexResetCredit from randomV4Uuid, the
  session helper moved to src/platform/random-v4-uuid.ts.
- RefreshControl goes; the header refresh stays. ConfirmModal's
  onConfirm is optional for an OK-only notice.
- Stale journal comments and the mobile docs line are corrected.
- The edit.tsx page exclusion is dropped from this PR; the script sweep
  is re-measured for 15 routes.
- Root oxlint: escape the apostrophe; zod v4 strictObject.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): secure UUID fallback from expo-crypto; pin reset retry routing

- randomV4Uuid never falls back to Math.random: without crypto.randomUUID
  it builds the v4 UUID from ExpoCrypto.getRandomBytes, as the old reset
  key did. Tests get Node's secure bytes for expo-crypto from the setup
  file, since the native module has no Node build.
- Host tests: a new offer after a settled attempt spends again rather
  than replaying, and a retry naming a newer offer joins a resumed
  attempt in flight (one provider call, original key).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* chore: restore main's bytes for files the merge's pre-commit format touched

The merge commit's lint-staged run reformatted 27 files this PR does not
change. Committed without the hook so they match origin/main exactly.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): stand in for expo-crypto with a Node alias, not a setup-file mock

Anti-slop forbids module mocking outside test files. The vitest config
now aliases expo-crypto to a faithful Node implementation of the names
shared code reaches; a test file's own mock still wins.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): match the bridged-parity comments to the measured 795-golden tally

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-17 03:11:24 -07:00
..

@orca/docs

This package contains the product documentation and public media intended to ship alongside Orca's source code.

Open-source product documentation for Orca, served at /docs (same URL shape as https://www.onorca.dev/docs).

This package is a self-contained Next.js app. It is intentionally not a root monorepo workspace member, so installing Electron app dependencies does not pull Next/fumadocs.

Local development

cd docs/site
pnpm --ignore-workspace install
pnpm --ignore-workspace dev

Open http://localhost:3004/docs.

Production build

cd docs/site
pnpm --ignore-workspace install
pnpm --ignore-workspace build
pnpm --ignore-workspace start

pnpm start serves the production build on port 3004. Paths:

Path Purpose
/ Redirects to /docs
/docs Docs index
/docs/... Nested doc pages from content/docs
/docs/api/search Fumadocs search index
/docs/og/... Per-page Open Graph image route

Layout

  • content/docs/ — MDX pages + meta.json navigation
  • public/docs/ — docs-only media, logo, and favicon (GIFs, posters, screenshots)
  • src/app/docs/ — fumadocs routes, OG images
  • src/components/ — docs-scoped chrome (header/footer/search), not marketing site

Updating content

Treat the documentation tree as a deliberate publication boundary. Before importing source material, review the diff for internal references, credentials, third-party media rights, and feature/version claims, then copy only approved pages and assets. Keep the app shell and deployment configuration in this repository so a docs-only pull request can be reviewed and built independently.

Same-domain routing

The docs app is a separate Vercel project (the docs zone) and keeps the public /docs URL namespace. The marketing site remains the default zone for www.onorca.dev; configure its Next/Vercel proxy with these beforeFiles rewrites, replacing DOCS_ORIGIN with the docs project's production URL:

return {
  beforeFiles: [
    {
      source: '/docs',
      destination: `${DOCS_ORIGIN}/docs`
    },
    {
      source: '/docs/:path*',
      destination: `${DOCS_ORIGIN}/docs/:path*`
    },
    {
      source: '/docs-static/:path*',
      destination: `${DOCS_ORIGIN}/docs-static/:path*`
    }
  ]
}

/docs-static is the docs zone's assetPrefix; it prevents _next asset collisions with the marketing zone. Keep the rewrites in the default zone and use ordinary <a> links when navigating between zones. Do not add basePath: '/docs' to this app: its route tree and Fumadocs baseUrl already include /docs, so doing so would publish /docs/docs/... URLs. If a future deployment needs basePath, first move the route tree to an unprefixed src/app/[[...slug]] shape and update every generated/link URL together.

Deploy (Vercel)

  1. Create a Vercel project with Root Directory unset (.). The workflow invokes Vercel from docs/site, so that directory is already the deployment root; setting it again would resolve docs/site/docs/site. Leave automatic Git deployments disabled so this workflow remains the only deployment path.
  2. Framework preset: Next.js. Use pnpm --ignore-workspace install --frozen-lockfile for install and pnpm --ignore-workspace build for build; this package has its own lockfile beside the root workspace.
  3. Set GitHub Actions secrets (required by the production deploy job):
    • VERCEL_TOKEN
    • VERCEL_ORG_ID
    • VERCEL_PROJECT_ID (docs project, not the marketing site)
  4. Protect the docs-production GitHub environment with required reviewers and custom deployment branch policies for main and v* tags. The release-cut dispatch runs from main; the direct published-release fallback runs from a stable tag, while the workflow still authorizes only exact stable tags.
  5. Prepare the three rewrites above in the www.onorca.dev marketing project, but leave them disabled until the docs deployment is verified. A Vercel custom domain cannot delegate only /docs by itself; the default zone must proxy both page/API/media requests and /docs-static assets. Keep the marketing project's old docs routes available for rollback during the transition; putting the proxy rules in beforeFiles ensures they win once enabled.
  6. Deploy a stable desktop tag that contains docs/site, verify the docs origin, then enable the marketing rewrites. Tags cut before this package was added cannot bootstrap the docs project because production intentionally checks out the tag's exact commit. Remove the old marketing docs routes in a follow-up after the proxy is stable.

.github/workflows/docs.yml runs credential-free checks for every pull request. It intentionally does not deploy PR previews: a PR-controlled build must not receive Vercel credentials. A maintainer can add a separate trusted preview workflow later. Production deploys only from an authorized stable desktop release: an exact vX.Y.Z tag, a published non-prerelease release, and the github-actions[bot] release author. Manual dispatch must run from the default branch and name an existing release that meets the same checks. Mobile, prerelease, draft, and human-authored releases are skipped. Fork pull requests remain build-only because GitHub does not expose deployment secrets to fork jobs.

Versioning

versioning.config.ts keeps the current unversioned /docs URLs stable while reserving content/versions/<id> for future snapshots. Versioned routes are not live yet; when they are needed, add the corresponding loader/routes and a version entry. The release workflow can then deploy them without a trigger change.

Isolation from the desktop app

  • Own package.json + pnpm-lock.yaml under docs/site/
  • Not listed in the root pnpm-workspace.yaml; install with pnpm --ignore-workspace
  • Engineering notes remain in repo-root docs/ — do not confuse with this package