Files
mobile.derez.ai/README.md
T
oliver ea1aeed1da Prioritize external transcription endpoint over browser Web Speech API
Previously the on-device Web Speech API (Chrome/Edge/Safari) was always
preferred, and the configured external transcription endpoint was only
used as a fallback when the browser had no speech API at all (Firefox).

Flip the priority in Composer.tsx: if a transcription endpoint is
configured in Settings, use it for dictation regardless of browser
support, since it's generally far more accurate. Fall back to the
browser's on-device SpeechRecognition only when no endpoint is set.

Update the Settings hint text and README to describe the new behavior.
Rebuilt and redeployed to the repo root.
2026-11-04 14:30:51 -03:00

9.2 KiB

Hermes PWA

A mobile-first Progressive Web App front end for a self-hosted Hermes Agent (Nous Research). It talks directly to the Hermes Agent API Server — the OpenAI-compatible HTTP + SSE endpoint — so you can chat with your agent, browse sessions, and use text, voice, and images from an installable app.

Branding (logo, colour scheme, app icons) follows derez.ai; the logo in the sidebar links to the homepage.


Which URL do I use? (dashboard vs API server)

Hermes exposes two different servers. This app needs the second one.

URL What it is Auth Works with this app?
https://your-host/ (e.g. beaver.derez.ai) The web dashboard username + password → session cookie ❌ No. It only allows CORS from its own origin and never sends Access-Control-Allow-Credentials: true, so a separately-hosted app can't use its cookie.
https://api.your-host/ (e.g. api.beaver.derez.ai, default port 8642) The API Server Authorization: Bearer <API_SERVER_KEY> ✅ Yes — this is the one.

So enter the API server URL and your API_SERVER_KEY on the config screen. There is no QR login and no username/password here — the API server uses a bearer token.

Enabling the API server (on the Hermes host)

If it isn't already running, add to ~/.hermes/.env and start the gateway:

API_SERVER_ENABLED=true
API_SERVER_KEY=choose-a-long-random-secret
# Only needed if a browser calls the API directly (see CORS below):
# API_SERVER_CORS_ORIGINS=https://your-pwa-origin
hermes gateway      # → [API Server] API server listening on http://127.0.0.1:8642

Verify from anywhere:

curl -s https://api.your-host/health
# {"status": "ok", "platform": "hermes-agent", "version": "..."}

curl -s -H "Authorization: Bearer $API_SERVER_KEY" https://api.your-host/v1/capabilities

Reaching the API server (CORS)

The API server has no browser CORS by default. Two options:

server/proxy.mjs serves the built app and proxies /api/* and /v1/* to the API server, so everything is same-origin and CORS never applies.

You normally paste the API key into the app in the browser (it's saved in that browser's localStorage). The proxy does not need the key:

HERMES_TARGET=https://api.beaver.derez.ai PORT=8787 ./run.sh serve
# open http://localhost:8787

Then in the app's config screen, tick "Same origin (behind the bundled proxy)" and paste your API key. Done — no key on the server, no CORS.

HERMES_API_KEY is optional. Only set it if you'd rather keep the key on the proxy so it never reaches the browser:

HERMES_TARGET=https://api.beaver.derez.ai HERMES_API_KEY=your-key PORT=8787 ./run.sh serve

Option B — direct, cross-origin

Set an explicit allowlist on the Hermes host and enter the API URL + key in the app's config screen:

API_SERVER_CORS_ORIGINS=https://your-pwa-origin

For local development you can skip CORS entirely with a dev proxy:

# .env
HERMES_DEV_TARGET=https://api.beaver.derez.ai

Quick start

./run.sh            # dev server -> http://localhost:5173

Then on the config screen enter your API server URL and API key.

run.sh modes

./run.sh              # dev server (http://localhost:5173)
./run.sh build        # production build, deployed straight into the repo root
./run.sh serve        # build if needed, then host + proxy /api/*, /v1/* (needs HERMES_TARGET)
./run.sh preview      # build, then serve the build with vite preview
./run.sh icons        # regenerate the PWA icon set
./run.sh typecheck    # type-check without emitting

Deployment model: the repo root is the web root

The Vite project lives in app/. Running a build (./run.sh build or cd app && npm run build) compiles it and then copies the output (index.html, assets/, manifest.webmanifest, sw.js, workbox-*.js, favicon.svg, icons/, LICENSE) directly into the repository root — see app/scripts/deploy.mjs. Those deployed files are committed to git.

This means a webserver that simply serves the raw repository checkout as static files (no build step, no custom MIME types, no reverse-proxy config) works out of the box: index.html in the repo root already references hashed, plain .js/.css assets — nothing points at /src/*.tsx in production, so there's no "unauthorized MIME type" issue.

To publish a new version:

cd app
npm run build      # builds app/dist, then syncs it into the repo root
cd ..
git add -A
git commit -m "Deploy: <description>"
git push            # or however the server pulls the repo

Note: app/ (source, dev config, node_modules) is still present in the checkout, so if the webserver serves the whole repo folder, the raw TypeScript source under app/src is technically reachable as plain text. It is never executed as a module by the deployed app (the deployed index.html only references the built assets/*.js), so this is not a functional issue — just something to be aware of if source privacy matters to you.

Variable Used by Meaning
HERMES_TARGET serve API server base URL to proxy to
HERMES_API_KEY serve Optional — injected as Authorization: Bearer …
PORT serve Port to listen on (default 8787)
HERMES_DEV_TARGET dev API server base URL to proxy to during development

Features

  • Config screen — add one or more API-server connections (URL, API key, optional profile prefix, custom headers) with a "Test" probe.
  • Sessions — list, open, create, rename, fork, and delete; filter by title.
  • Streaming chat — token-by-token replies over SSE, with live tool-call cards, cancel, and mid-turn steer.
  • Images — attach/paste/drag-drop; sent inline as data: URLs.
  • Voice — dictation via the browser's Web Speech API and spoken replies via on-device speech synthesis. Tap the mic to speak your prompt; tap again to stop. Requires a secure context (HTTPS or localhost) and a browser with the Web Speech API (Chrome, Edge, Safari). Note: Chrome's dictation sends audio to Google's servers.
  • Installable PWA — offline app shell, home-screen install, standalone display.

Notes & limitations

  • Voice is client-side. The API server has no /transcribe or /tts endpoint. If a transcription endpoint is configured in Settings (any OpenAI-compatible /v1/audio/transcriptions server), it's used for dictation; otherwise dictation falls back to the browser's on-device SpeechRecognition. Spoken replies always use speechSynthesis. Support varies by browser (best on Chrome/Edge/Safari).
  • Profiles. The API server routes profiles via a /p/<profile>/… prefix, and each profile has its own API_SERVER_KEY. Add one connection per profile to see them all; the sidebar switches between connections.
  • Session ownership. The API server can create and drive its own sessions, but it refuses to write to a session owned by another surface (CLI, TUI, dashboard, messaging) — those open read-only.
  • Response shapes. The Sessions REST API's exact JSON envelope isn't fully documented, so the client normalises several shapes (sessions/data/items, id/session_id, string/array content). If a field looks off on your version, it's a one-line tweak in app/src/api/client.ts.

Project layout

index.html, assets/, manifest.webmanifest,    # repo root = deployed build
sw.js, workbox-*.js, favicon.svg, icons/,     # output (see Deployment model above)
LICENSE
app/                 # Vite project (source, dev config)
  index.html         # dev entry (-> /src/main.tsx); NOT served in production
  src/
    api/
      client.ts        # HermesClient: bearer auth, sessions REST, SSE chat
      types.ts         # API server types + normalised UI types
    hooks/
      useSpeechRecognition.ts  # browser dictation
      useTts.ts                # browser speech synthesis
    lib/
      config.ts        # saved connections (localStorage)
      format.ts        # time / token formatting
      markdown.tsx     # tiny safe markdown renderer
    state/
      store.ts         # zustand store: connections, sessions, streaming
    components/        # ConfigScreen, ConnectionForm, Sidebar, ChatView, Composer, …
  scripts/
    gen-icons.mjs      # dependency-free PNG icon generator
    deploy.mjs         # copies app/dist/* into the repo root after `vite build`
  server/
    proxy.mjs          # optional static host + /api,/v1 reverse proxy (no deps)

License

Licensed under the PolyForm Internal Use License 1.0.0 — internal business use only; redistribution is not permitted. The license text is also shipped in the build output as dist/LICENSE.

API endpoints used

GET /health, GET /v1/capabilities, GET /v1/models, GET|POST /api/sessions, GET|PATCH|DELETE /api/sessions/{id}, GET /api/sessions/{id}/messages, POST /api/sessions/{id}/fork, POST /api/sessions/{id}/chat/stream (SSE), and POST /v1/runs/{id}/stop|steer|approval|clarify.