@pollen-robotics/reachy-mini-sdk 1.7.3-main.7457b2b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/LICENSE +201 -0
  3. package/README.md +129 -0
  4. package/host/APP_AUTHOR_GUIDE.md +646 -0
  5. package/host/README.md +155 -0
  6. package/host/SPEC.md +618 -0
  7. package/host/dist/ReachyHost.d.ts +39 -0
  8. package/host/dist/ReachyHost.d.ts.map +1 -0
  9. package/host/dist/assets/index.d.ts +16 -0
  10. package/host/dist/assets/index.d.ts.map +1 -0
  11. package/host/dist/chunks/index-C3B3FuBC.js +402 -0
  12. package/host/dist/chunks/index-C3B3FuBC.js.map +1 -0
  13. package/host/dist/chunks/mountHost-8f-laxwI.js +48280 -0
  14. package/host/dist/chunks/mountHost-8f-laxwI.js.map +1 -0
  15. package/host/dist/chunks/reachy-mini-sdk-T3Zr7Hwc.js +1726 -0
  16. package/host/dist/chunks/reachy-mini-sdk-T3Zr7Hwc.js.map +1 -0
  17. package/host/dist/components/ConnectingView.d.ts +13 -0
  18. package/host/dist/components/ConnectingView.d.ts.map +1 -0
  19. package/host/dist/components/EmbedFrame.d.ts +16 -0
  20. package/host/dist/components/EmbedFrame.d.ts.map +1 -0
  21. package/host/dist/components/ErrorView.d.ts +21 -0
  22. package/host/dist/components/ErrorView.d.ts.map +1 -0
  23. package/host/dist/components/LeavingView.d.ts +33 -0
  24. package/host/dist/components/LeavingView.d.ts.map +1 -0
  25. package/host/dist/components/PickerView.d.ts +51 -0
  26. package/host/dist/components/PickerView.d.ts.map +1 -0
  27. package/host/dist/components/ReachyHostShell.d.ts +21 -0
  28. package/host/dist/components/ReachyHostShell.d.ts.map +1 -0
  29. package/host/dist/components/SignInView.d.ts +32 -0
  30. package/host/dist/components/SignInView.d.ts.map +1 -0
  31. package/host/dist/components/StepsProgressIndicator.d.ts +41 -0
  32. package/host/dist/components/StepsProgressIndicator.d.ts.map +1 -0
  33. package/host/dist/components/TopBar.d.ts +52 -0
  34. package/host/dist/components/TopBar.d.ts.map +1 -0
  35. package/host/dist/components/WelcomeBackOverlay.d.ts +28 -0
  36. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -0
  37. package/host/dist/embed/index.d.ts +150 -0
  38. package/host/dist/embed/index.d.ts.map +1 -0
  39. package/host/dist/entry/auto.d.ts +12 -0
  40. package/host/dist/entry/auto.d.ts.map +1 -0
  41. package/host/dist/entry/auto.js +7 -0
  42. package/host/dist/entry/auto.js.map +1 -0
  43. package/host/dist/entry/embed.d.ts +12 -0
  44. package/host/dist/entry/embed.d.ts.map +1 -0
  45. package/host/dist/entry/embed.js +7 -0
  46. package/host/dist/entry/embed.js.map +1 -0
  47. package/host/dist/hooks/useHfProfile.d.ts +7 -0
  48. package/host/dist/hooks/useHfProfile.d.ts.map +1 -0
  49. package/host/dist/hooks/useHostBridge.d.ts +37 -0
  50. package/host/dist/hooks/useHostBridge.d.ts.map +1 -0
  51. package/host/dist/hooks/useOAuth.d.ts +16 -0
  52. package/host/dist/hooks/useOAuth.d.ts.map +1 -0
  53. package/host/dist/hooks/useRobots.d.ts +17 -0
  54. package/host/dist/hooks/useRobots.d.ts.map +1 -0
  55. package/host/dist/hooks/useSdk.d.ts +16 -0
  56. package/host/dist/hooks/useSdk.d.ts.map +1 -0
  57. package/host/dist/index.d.ts +25 -0
  58. package/host/dist/index.d.ts.map +1 -0
  59. package/host/dist/index.js +15 -0
  60. package/host/dist/index.js.map +1 -0
  61. package/host/dist/lib/centralListener.d.ts +73 -0
  62. package/host/dist/lib/centralListener.d.ts.map +1 -0
  63. package/host/dist/lib/centralRest.d.ts +35 -0
  64. package/host/dist/lib/centralRest.d.ts.map +1 -0
  65. package/host/dist/lib/protocol.d.ts +230 -0
  66. package/host/dist/lib/protocol.d.ts.map +1 -0
  67. package/host/dist/lib/protocol.js +48 -0
  68. package/host/dist/lib/protocol.js.map +1 -0
  69. package/host/dist/lib/sdk-types.d.ts +46 -0
  70. package/host/dist/lib/sdk-types.d.ts.map +1 -0
  71. package/host/dist/lib/settings.d.ts +69 -0
  72. package/host/dist/lib/settings.d.ts.map +1 -0
  73. package/host/dist/lib/signalingUrl.d.ts +28 -0
  74. package/host/dist/lib/signalingUrl.d.ts.map +1 -0
  75. package/host/dist/lib/theme.d.ts +4 -0
  76. package/host/dist/lib/theme.d.ts.map +1 -0
  77. package/host/dist/lib/themeMode.d.ts +9 -0
  78. package/host/dist/lib/themeMode.d.ts.map +1 -0
  79. package/host/dist/lib/tokens.d.ts +64 -0
  80. package/host/dist/lib/tokens.d.ts.map +1 -0
  81. package/host/dist/mountHost.d.ts +36 -0
  82. package/host/dist/mountHost.d.ts.map +1 -0
  83. package/package.json +109 -0
  84. package/reachy-mini-sdk.d.ts +285 -0
  85. package/reachy-mini-sdk.js +2625 -0
@@ -0,0 +1,646 @@
1
+ # Build a Reachy Mini app
2
+
3
+ How to ship a Hugging Face Space that runs on a Reachy Mini robot,
4
+ using `@pollen-robotics/reachy-mini-sdk/host` for OAuth, robot picking, session
5
+ lifecycle, and a top bar - so your code stays focused on **your
6
+ app's UI** and nothing else.
7
+
8
+ | Doc | Purpose | Audience |
9
+ |------------|--------------------------------------|-----------------------------------|
10
+ | README | Quick tour of the package | First-time visitors |
11
+ | **This** | **Step-by-step app author guide** | **You, building a new app** |
12
+ | SPEC.md | Behavioural contract + invariants | Maintainers, reviewers |
13
+
14
+ ## Table of contents
15
+
16
+ 1. [What you get for free](#1-what-you-get-for-free)
17
+ 2. [Quickstart: clone a reference app](#2-quickstart-clone-a-reference-app)
18
+ 3. [The 4-file contract](#3-the-4-file-contract)
19
+ 4. [`mountHost()` API](#4-mounthost-api)
20
+ 5. [`connectToHost()` API](#5-connecttohost-api)
21
+ 6. [Visual identity: icon, name, emoji](#6-visual-identity-icon-name-emoji)
22
+ 7. [Receiving an external config (deep-link, mobile)](#7-receiving-an-external-config)
23
+ 8. [Cleaning up on leave](#8-cleaning-up-on-leave)
24
+ 9. [Local dev: HF token vs OAuth redirect](#9-local-dev)
25
+ 10. [Deploying to Hugging Face Spaces](#10-deploying-to-hugging-face-spaces)
26
+ 11. [FAQ and common pitfalls](#11-faq-and-common-pitfalls)
27
+
28
+ ---
29
+
30
+ ## 1. What you get for free
31
+
32
+ By integrating `@pollen-robotics/reachy-mini-sdk/host`, your app **does not have to
33
+ write**:
34
+
35
+ - **Hugging Face OAuth**: sign-in screen, redirect handling,
36
+ token storage, sign-out menu - all in the host shell.
37
+ - **Robot discovery and picker**: list of online robots, live
38
+ online/offline/busy updates, click-to-pick.
39
+ - **Connection overlay**: the 3-step "Connecting / Starting
40
+ session / Waking up" view rendered on top of your iframe.
41
+ - **End-session button and tear-down**: a "Back to apps" affordance
42
+ in the top bar that cleanly closes the WebRTC session.
43
+ - **Dark / light theme switching**: respected from
44
+ `prefers-color-scheme` or HF settings, propagated to your iframe.
45
+
46
+ What **you** write:
47
+
48
+ - `index.html` + `Dockerfile` (boilerplate, ~10 lines each).
49
+ - `src/dispatch.ts` (~20 lines, picks shell vs embed mode).
50
+ - `src/embed.{ts,tsx}` - **your app's actual code**. You receive a
51
+ live `ReachyMini` SDK handle and render whatever you want.
52
+
53
+ You can use **any framework** inside your `embed` entry: React,
54
+ Svelte, Vue, vanilla TS. The host runs outside your iframe and
55
+ doesn't care.
56
+
57
+ ---
58
+
59
+ ## 2. Quickstart: clone a reference app
60
+
61
+ Pick the reference closest to your needs and clone its `index.html`
62
+ + `dispatch.ts` + `Dockerfile`:
63
+
64
+ | Reference app | Stack | Use it for |
65
+ |-------------------------------------|-----------------------------|---------------------------------------------|
66
+ | `reachy_mini_emotions/` | React 19 + MUI 7 + Vite | UI-rich app with rich components / theming |
67
+ | `reachy_mini_telepresence/` | React 19 + MUI 7 + Vite | App with camera / media streams |
68
+ | `reachy_mini_minimal_conversation/` | **Vanilla TS + Vite** | Smallest runtime, no framework |
69
+
70
+ ```bash
71
+ # Example: start from the vanilla TS template
72
+ cp -r reachy_mini_minimal_conversation/ my_new_app/
73
+ cd my_new_app
74
+ # Edit package.json `name`, README frontmatter (`title`, `emoji`),
75
+ # and src/embed.ts to your app.
76
+ npm install
77
+ npm run dev
78
+ # → http://localhost:5173
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 3. The 4-file contract
84
+
85
+ These four files are the entire integration surface. Don't add
86
+ mandatory files outside this list - keep the apps interchangeable.
87
+
88
+ ### 3.1 `index.html`
89
+
90
+ ```html
91
+ <!doctype html>
92
+ <html lang="en">
93
+ <head>
94
+ <meta charset="UTF-8" />
95
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
96
+ <title>My App</title>
97
+ <link rel="icon" href="/icon.svg" type="image/svg+xml" />
98
+
99
+ <!-- Theme bootstrap: paints the right palette before CSS lands -->
100
+ <script>
101
+ (function () {
102
+ var params = new URLSearchParams(window.location.search);
103
+ var raw = params.get("theme");
104
+ var mode = (raw === "dark" || raw === "light")
105
+ ? raw
106
+ : (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
107
+ document.documentElement.setAttribute("data-theme", mode);
108
+ document.documentElement.style.backgroundColor = mode === "dark" ? "#101013" : "#fafafa";
109
+ })();
110
+ </script>
111
+
112
+ <!-- HF Spaces injects OAuth client ID at container start -->
113
+ <script>
114
+ (function () {
115
+ var clientId = "__OAUTH_CLIENT_ID__";
116
+ if (clientId && clientId.indexOf("__") !== 0) {
117
+ window.huggingface = { variables: { OAUTH_CLIENT_ID: clientId } };
118
+ }
119
+ })();
120
+ </script>
121
+
122
+ <!-- ReachyMini SDK, pinned to a MAJOR version (e.g. @1) for auto-patch
123
+ updates, or to a TAG / SHA via `cdn.jsdelivr.net/gh/...` if you
124
+ need bleeding-edge. The `+esm` suffix resolves the bare
125
+ @huggingface/hub dependency recursively. -->
126
+ <script type="module">
127
+ import { ReachyMini } from "https://cdn.jsdelivr.net/npm/@pollen-robotics/reachy-mini-sdk@1/+esm";
128
+ window.ReachyMini = ReachyMini;
129
+ window.dispatchEvent(new Event("reachymini:ready"));
130
+ </script>
131
+ </head>
132
+ <body>
133
+ <div id="root"></div>
134
+ <script type="module" src="/src/dispatch.ts"></script>
135
+ </body>
136
+ </html>
137
+ ```
138
+
139
+ ### 3.2 `src/dispatch.ts`
140
+
141
+ ```ts
142
+ const params = new URLSearchParams(window.location.search);
143
+
144
+ if (params.get('embedded') === '1') {
145
+ void import('./embed');
146
+ } else {
147
+ void import('@pollen-robotics/reachy-mini-sdk/host/auto').then(({ mountHost }) => {
148
+ mountHost({
149
+ appName: 'My App',
150
+ appIconUrl: '/icon.svg',
151
+ enableMicrophone: false,
152
+ });
153
+ });
154
+ }
155
+ ```
156
+
157
+ ### 3.3 `src/embed.ts` (vanilla example)
158
+
159
+ ```ts
160
+ import { connectToHost } from '@pollen-robotics/reachy-mini-sdk/host/embed';
161
+
162
+ interface MyConfig {
163
+ startingEmotion?: string;
164
+ }
165
+
166
+ async function main() {
167
+ const handle = await connectToHost<MyConfig>();
168
+ const { reachy, theme, config, onLeave } = handle;
169
+
170
+ document.body.innerHTML = '<h1>Connected!</h1>';
171
+
172
+ reachy.setHeadRpyDeg(0, 10, 0);
173
+
174
+ onLeave(async () => {
175
+ document.body.innerHTML = '<p>Bye!</p>';
176
+ });
177
+ }
178
+
179
+ void main().catch((err) => {
180
+ console.error('[my-app] boot failed', err);
181
+ window.parent.postMessage(
182
+ { source: 'reachy-mini', type: 'embed:error', version: 1,
183
+ message: String(err), fatal: true },
184
+ window.location.origin,
185
+ );
186
+ });
187
+ ```
188
+
189
+ ### 3.4 `Dockerfile`
190
+
191
+ Standard nginx + node build. Copy from `reachy_mini_emotions/Dockerfile`
192
+ verbatim; the only thing to customise is the app name in comments.
193
+
194
+ ---
195
+
196
+ ## 4. `mountHost()` API
197
+
198
+ Called once from `dispatch.ts` when the URL is **not** in embed mode.
199
+ Renders the shell into `#root`. The shell's visual theme (MUI
200
+ light/dark) is bundled and not overridable - the host owns its
201
+ look, apps own theirs inside the iframe.
202
+
203
+ ```ts
204
+ import { mountHost } from '@pollen-robotics/reachy-mini-sdk/host/auto';
205
+
206
+ mountHost({
207
+ appName: 'My App', // REQUIRED: passed to the SDK + shown in top bar
208
+ appIconUrl: '/icon.svg', // optional: top-bar logo (see §6)
209
+ appEmoji: '🤖', // optional: fallback when no icon.svg
210
+ enableMicrophone: false, // false unless you need WebRTC audio in
211
+ clientId: undefined, // optional: HF OAuth client ID; defaults to window.huggingface.variables.OAUTH_CLIENT_ID
212
+ devToken: undefined, // optional: { token, userName } - dev shortcut, see §9
213
+ target: undefined, // optional: HTMLElement | string CSS selector; default '#root'
214
+ });
215
+ ```
216
+
217
+ **Required**: `appName`. Everything else has sensible defaults.
218
+
219
+ **Return**: `{ dispose(): void }` - call to unmount cleanly. You
220
+ usually never need this; the page lifecycle handles it.
221
+
222
+ ---
223
+
224
+ ## 5. `connectToHost()` API
225
+
226
+ Called once from `embed.{ts,tsx}` to get a live SDK handle.
227
+
228
+ ```ts
229
+ import { connectToHost } from '@pollen-robotics/reachy-mini-sdk/host/embed';
230
+
231
+ interface MyConfig { /* whatever your app accepts */ }
232
+
233
+ const handle = await connectToHost<MyConfig>();
234
+ ```
235
+
236
+ Awaiting `connectToHost()` blocks until:
237
+
238
+ 1. The URL hash creds are parsed and wiped.
239
+ 2. The SDK script loaded (`window.ReachyMini` ready).
240
+ 3. The host posted `host:init` (Mode A only; Mode B times out
241
+ after 8 s and proceeds from hash alone).
242
+ 4. The SDK connected, started a session, and woke the robot.
243
+
244
+ The resolved `handle` exposes:
245
+
246
+ ```ts
247
+ interface ConnectedHandle<TConfig> {
248
+ // Live state at boot
249
+ reachy: ReachyMiniInstance; // SDK instance, session live, robot awake
250
+ theme: 'dark' | 'light';
251
+ config: TConfig | null;
252
+ appName: string;
253
+ hostName: string;
254
+ userName: string | null;
255
+
256
+ // Subscribe to live updates from the host (returns an unsub fn)
257
+ onLeave(cb: () => void | Promise<void>): () => void;
258
+ onThemeChange(cb: (theme: 'dark' | 'light') => void): () => void;
259
+ onConfigChange(cb: (config: TConfig | null) => void): () => void;
260
+
261
+ // Push state / requests back to the host
262
+ setAppState(s: { phase, connectingStep?, message? }): void;
263
+ requestLeave(): void; // ask host to end the session
264
+ reportError(msg: string, opts?: { fatal?, detail? }): void;
265
+ }
266
+ ```
267
+
268
+ The API is intentionally minimal. If you need a custom channel
269
+ between host and embed, file a feature request - we'll add it as
270
+ a typed message rather than expose a free-form sink.
271
+
272
+ ### Typing your config
273
+
274
+ `connectToHost<T>()` types the `config` field; runtime validation
275
+ is **your job**. An attacker controlling the URL can shape config
276
+ freely - cast is not enough.
277
+
278
+ ```ts
279
+ const handle = await connectToHost<MyConfig>();
280
+ const config = isValidConfig(handle.config) ? handle.config : null;
281
+ ```
282
+
283
+ ---
284
+
285
+ ## 6. Visual identity: icon, name, emoji
286
+
287
+ > **App identity is your HF Space ID** (`owner/space`). An app
288
+ > published at `huggingface.co/spaces/your-name/cool-thing` has
289
+ > identity `your-name/cool-thing` everywhere downstream (mobile
290
+ > catalog, "last opened", official badge). Apps in the
291
+ > `pollen-robotics/*` namespace are automatically tagged as
292
+ > official in the catalog; no extra config.
293
+
294
+ Your app's identity surfaces in **three independent places**, all
295
+ fed from the same sources but each by its own resolution path:
296
+
297
+ | Surface | What it shows | How it gets there |
298
+ |-------------------------------|----------------------------------------|----------------------------------------------------------------------------------------------------|
299
+ | Host top bar (in your iframe) | App logo + app name | You pass `appName` + `appIconUrl` + `appEmoji` to `mountHost()` in `dispatch.ts` |
300
+ | Hugging Face mobile catalog | App tile (icon + title + description) | The mobile reads the catalog API; the API reads HF Spaces' frontmatter + probes `public/icon.svg` |
301
+ | Browser tab / OS | Favicon | Your `index.html` `<link rel="icon" href="/icon.svg">` |
302
+
303
+ The host shell itself **does not discover or list other apps**.
304
+ It renders only the app it lives in. The catalog is owned by the
305
+ mobile and the website API; see §11 FAQ for the API spec if you
306
+ need to validate your app shows up.
307
+
308
+ ### Single source of truth: one `icon.svg`, one Space frontmatter
309
+
310
+ You ship **one** SVG file and **one** README frontmatter; both
311
+ the host and the mobile catalog read from them:
312
+
313
+ 1. **`public/icon.svg`** in your repo. Vite copies it verbatim to
314
+ `dist/icon.svg`, where nginx serves it at the root URL of your
315
+ Space. Reference it from `index.html`:
316
+
317
+ ```html
318
+ <link rel="icon" href="/icon.svg" type="image/svg+xml" />
319
+ ```
320
+
321
+ Pass it to `mountHost()` so the top bar renders it without a
322
+ probe:
323
+
324
+ ```ts
325
+ mountHost({ appName: 'My App', appIconUrl: '/icon.svg' });
326
+ ```
327
+
328
+ The catalog API also picks it up by listing the Space's
329
+ files (`siblings`) and matching `public/icon.svg` (or
330
+ `public/icon.png`), no live probe required.
331
+
332
+ 2. **HF Space frontmatter** in your `README.md`:
333
+
334
+ ```yaml
335
+ ---
336
+ title: My App
337
+ emoji: 🤖
338
+ colorFrom: yellow
339
+ colorTo: red
340
+ sdk: docker
341
+ app_port: 7860
342
+ pinned: false
343
+ hf_oauth: true
344
+ short_description: One-line description shown in the catalog.
345
+ tags:
346
+ - reachy_mini
347
+ - reachy_mini_js_app
348
+ ---
349
+ ```
350
+
351
+ - `title` is the app name in the mobile catalog (the in-iframe
352
+ top bar uses `appName` from `mountHost()` instead).
353
+ - `emoji` is the fallback logo when no `icon.svg` is shipped.
354
+ Pass the same value to `mountHost({ appEmoji: '🤖' })` so the
355
+ top bar's fallback matches.
356
+ - `short_description` shows under the app tile in the catalog.
357
+ - The **`reachy_mini_js_app` tag is mandatory** to appear in
358
+ the mobile catalog. The catalog API filters on this exact
359
+ string. Don't remove it.
360
+ - `hf_oauth: true` makes HF auto-provision an OAuth client and
361
+ inject the ID at container start.
362
+
363
+ ### Icon design recommendations
364
+
365
+ Your icon renders at three different sizes; design for all three:
366
+
367
+ - **Host top bar inside the iframe**: ~24x24 px square.
368
+ - **Mobile catalog tile**: ~64x64 px square card.
369
+ - **Browser tab favicon**: 16x16 px.
370
+
371
+ Practical guidance:
372
+
373
+ - Use a **square viewBox** (e.g. `viewBox="0 0 24 24"`) so the
374
+ three target sizes all crop identically.
375
+ - Keep the icon **readable at 16 px**: thick strokes, simple
376
+ silhouette, max 2-3 distinct shapes.
377
+ - Inline all colours; **don't reference external CSS**, the icon
378
+ is served standalone.
379
+ - Respect dark and light backgrounds: an icon that vanishes on
380
+ light should provide a `<style>` tag with `@media (prefers-color-scheme)`
381
+ rules or, simpler, use a neutral mid-tone palette.
382
+ - **Optimise the SVG**: target ~30 KB or less. Tools: `svgo`,
383
+ Figma's "Export SVG → optimise".
384
+
385
+ ### PNG fallback
386
+
387
+ If you can't ship SVG (heavy raster art, exported portrait, ...),
388
+ the catalog API also accepts `public/icon.png`. SVG wins when both
389
+ exist. The host top bar only renders the SVG variant - if your
390
+ `mountHost({ appIconUrl })` points at a PNG, it works, but you
391
+ lose the crisp upscale on hi-DPI screens.
392
+
393
+ ---
394
+
395
+ ## 7. Receiving an external config
396
+
397
+ The host accepts a base64-encoded JSON `config` from two sources:
398
+
399
+ 1. **URL parameter**: `https://<space>.hf.space/?config=eyJlbW90aW9uIjoiam95In0=`
400
+ (decoded once, passed verbatim).
401
+ 2. **Mobile handoff**: the mobile app embeds your Space with
402
+ `?embedded=1#creds=<base64-bundle-including-config>`.
403
+
404
+ Your app receives `config` typed as `unknown`; cast and validate.
405
+
406
+ ```ts
407
+ interface MyConfig { startingEmotion?: string; }
408
+
409
+ function isMyConfig(v: unknown): v is MyConfig {
410
+ return v != null && typeof v === 'object' && (
411
+ (v as MyConfig).startingEmotion === undefined ||
412
+ typeof (v as MyConfig).startingEmotion === 'string'
413
+ );
414
+ }
415
+
416
+ const handle = await connectToHost<MyConfig>();
417
+ const initial: MyConfig = isMyConfig(handle.config) ? handle.config : {};
418
+
419
+ handle.onConfigChange((next) => {
420
+ if (isMyConfig(next)) /* react to it */;
421
+ });
422
+ ```
423
+
424
+ If your app's UI state changes in a way the mobile would want to
425
+ remember (e.g. user picked a different emotion), persist it in
426
+ your app's storage. The host does **not** propagate state
427
+ upstream - apps don't push config to the host in v1.
428
+
429
+ ---
430
+
431
+ ## 8. Cleaning up on leave
432
+
433
+ The host fires a tear-down sequence in three scenarios:
434
+
435
+ - User clicks "End session" / "Back to apps" in the top bar.
436
+ - Your app calls `handle.requestLeave()`.
437
+ - The page is unloaded (`pagehide`, e.g. user closes the tab).
438
+
439
+ In all three cases your `onLeave` callbacks fire. You have **~1.5-2 s**
440
+ before the host force-unmounts the iframe; use that to:
441
+
442
+ ```ts
443
+ handle.onLeave(async () => {
444
+ player.cancel(); // stop streaming motion frames
445
+ audioCtx?.close(); // release audio
446
+ ws?.close(); // close any side channels
447
+ await flushTelemetry(); // your async hooks
448
+ });
449
+ ```
450
+
451
+ You do **not** need to call `reachy.stopSession()` yourself - the
452
+ host does. You also don't need to navigate away; the iframe is
453
+ unmounted by the host.
454
+
455
+ ---
456
+
457
+ ## 9. Local dev
458
+
459
+ You have two options, picked by the `devToken` and `clientId`
460
+ props passed to `mountHost()`. Reference apps support both via
461
+ `.env.local`.
462
+
463
+ ### Option A: personal access token (no OAuth)
464
+
465
+ Fastest for local dev. Skips the OAuth redirect entirely.
466
+
467
+ 1. Get a token at <https://huggingface.co/settings/tokens> (read
468
+ scope is enough).
469
+ 2. Create `.env.local`:
470
+
471
+ ```
472
+ VITE_HF_TOKEN=hf_xxx
473
+ VITE_HF_USERNAME=your-handle
474
+ ```
475
+
476
+ 3. In `dispatch.ts`, forward both to `mountHost`:
477
+
478
+ ```ts
479
+ mountHost({
480
+ appName: 'My App',
481
+ devToken: import.meta.env.VITE_HF_TOKEN && import.meta.env.VITE_HF_USERNAME
482
+ ? { token: import.meta.env.VITE_HF_TOKEN, userName: import.meta.env.VITE_HF_USERNAME }
483
+ : undefined,
484
+ });
485
+ ```
486
+
487
+ 4. `npm run dev` → you're signed in on page load.
488
+
489
+ `.env.local` must be gitignored. **Never commit the token.**
490
+
491
+ ### Option B: real OAuth client ID
492
+
493
+ Use this when you're touching the OAuth / logout paths.
494
+
495
+ 1. Go to <https://huggingface.co/settings/applications/new>.
496
+ 2. Homepage URL: `http://localhost:5173` · Redirect URIs:
497
+ `http://localhost:5173` · Scopes: at least `openid`, `profile`.
498
+ 3. Copy the client ID into `.env.local`:
499
+
500
+ ```
501
+ VITE_HF_OAUTH_CLIENT_ID=...
502
+ ```
503
+
504
+ 4. Forward to `mountHost`:
505
+
506
+ ```ts
507
+ mountHost({
508
+ appName: 'My App',
509
+ clientId: import.meta.env.VITE_HF_OAUTH_CLIENT_ID,
510
+ });
511
+ ```
512
+
513
+ ---
514
+
515
+ ## 10. Deploying to Hugging Face Spaces
516
+
517
+ The reference apps' `Dockerfile` is ready to copy. Steps:
518
+
519
+ ```bash
520
+ # 1. Init the Space repo
521
+ huggingface-cli repo create my-app --type space --space_sdk docker
522
+
523
+ # 2. Push
524
+ git remote add space git@hf.co:spaces/<your-username>/my-app
525
+ git push space main
526
+ ```
527
+
528
+ HF Spaces will:
529
+
530
+ - Build your `Dockerfile` (the multi-stage `node:20-alpine` → `nginx:1.27-alpine` setup).
531
+ - At container start, run `docker-entrypoint.d/10-inject-hf-vars.sh`
532
+ to patch `index.html` with the real OAuth client ID, space host,
533
+ and space ID.
534
+ - Serve nginx on `app_port` (7860).
535
+
536
+ ### Required Space frontmatter
537
+
538
+ ```yaml
539
+ sdk: docker
540
+ app_port: 7860
541
+ hf_oauth: true
542
+ tags:
543
+ - reachy_mini
544
+ - reachy_mini_js_app # critical: mobile app discovery
545
+ ```
546
+
547
+ ### Cache busting
548
+
549
+ If after a push your Space still serves the old bundle, push an
550
+ empty commit:
551
+
552
+ ```bash
553
+ git commit --allow-empty -m "chore: bust HF Spaces cache"
554
+ git push space main
555
+ ```
556
+
557
+ ---
558
+
559
+ ## 11. FAQ and common pitfalls
560
+
561
+ ### "I see a `Robot is busy` error even though no one is using the robot"
562
+
563
+ The host's SDK and the embed's SDK both claim a peer at the
564
+ central. The host **must** disconnect when the embed boots; if it
565
+ doesn't, the central sees two peers with the same `appName` and
566
+ rejects the embed.
567
+
568
+ This is handled automatically by `@pollen-robotics/reachy-mini-sdk/host` (§8.1 of the
569
+ SPEC). If you see this in dev, you likely have **two tabs** open
570
+ on the same Space - that's expected behaviour.
571
+
572
+ ### "My app loads React + MUI even though I wrote vanilla TS"
573
+
574
+ The **host shell** is React + MUI. It runs **only outside your
575
+ iframe** (sign-in screen, picker, top bar). Once your app is live,
576
+ the host's React tree is idle.
577
+
578
+ Your iframe content is whatever you wrote. Vanilla TS apps stay
579
+ slim inside the iframe.
580
+
581
+ ### "Vite warns about React being installed in two places"
582
+
583
+ You're using the legacy `file:./vendor/reachy-mini-host` dep
584
+ pattern (now unsupported — the host ships from npm as part of
585
+ `@pollen-robotics/reachy-mini-sdk`). Migrate to the npm dep and,
586
+ if you still see the warning, add to your `vite.config.ts`:
587
+
588
+ ```ts
589
+ export default defineConfig({
590
+ resolve: {
591
+ dedupe: ['react', 'react-dom', 'react/jsx-runtime',
592
+ '@emotion/react', '@emotion/styled',
593
+ '@mui/material', '@mui/icons-material'],
594
+ },
595
+ optimizeDeps: {
596
+ include: ['@pollen-robotics/reachy-mini-sdk',
597
+ '@pollen-robotics/reachy-mini-sdk/host',
598
+ '@pollen-robotics/reachy-mini-sdk/host/auto',
599
+ '@pollen-robotics/reachy-mini-sdk/host/embed'],
600
+ },
601
+ });
602
+ ```
603
+
604
+ ### "I want a different sign-in flow"
605
+
606
+ Not supported in v1. The host owns OAuth. If you need a custom
607
+ flow, the standalone shell isn't for you - publish your Space
608
+ with the host disabled (just don't call `mountHost()`) and roll
609
+ your own.
610
+
611
+ ### "I want a different theme than the bundled MUI one"
612
+
613
+ The host shell's look is fixed (light + dark MUI bundle). Apps
614
+ own their own theme **inside the iframe** - use the
615
+ `handle.theme` value as your mode signal and wrap your app in
616
+ whatever ThemeProvider you want.
617
+
618
+ ```ts
619
+ const handle = await connectToHost();
620
+ // Mirror `handle.theme` ('dark' | 'light') in your own
621
+ // ThemeProvider. The host pushes updates via onThemeChange().
622
+ ```
623
+
624
+ ### "The icon doesn't show up in the top bar"
625
+
626
+ Check the three sources in priority order (§6):
627
+
628
+ 1. Is `/icon.svg` reachable at the deployed URL? Open
629
+ `https://<space>.hf.space/icon.svg` directly.
630
+ 2. Is the file's MIME type `image/svg+xml`? The host's probe
631
+ checks the response's `content-type`.
632
+ 3. Did you pass `appIconUrl: '/icon.svg'` to `mountHost()`?
633
+
634
+ If 1 + 2 + 3 are correct and it still fails, file a bug.
635
+
636
+ ### "How do I test the mobile-handoff mode locally?"
637
+
638
+ Hit your dev server at:
639
+
640
+ ```
641
+ http://localhost:5173/?embedded=1#creds=<base64({"hfToken":"hf_xxx","userName":"you","robotPeerId":"abc","signalingUrl":"https://...","theme":"dark","config":null,"hostName":"Reachy Mini","appName":"My App"})>
642
+ ```
643
+
644
+ The dispatcher will skip the shell and go straight to your embed.
645
+ Useful for testing the embed path without spinning up the mobile
646
+ app.