@mentra/miniapp 0.3.0-dev.0 → 3.1.0-dev.4

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 (106) hide show
  1. package/README.md +8 -6
  2. package/dist/background/index.d.ts +7 -4
  3. package/dist/background/index.d.ts.map +1 -1
  4. package/dist/background/index.js +1 -0
  5. package/dist/background/index.js.map +1 -1
  6. package/dist/background/register.d.ts +3 -2
  7. package/dist/background/register.d.ts.map +1 -1
  8. package/dist/background/register.js.map +1 -1
  9. package/dist/hardware.d.ts +24 -0
  10. package/dist/hardware.d.ts.map +1 -0
  11. package/dist/hardware.js +20 -0
  12. package/dist/hardware.js.map +1 -0
  13. package/dist/index.d.ts +8 -5
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +5 -5
  16. package/dist/index.js.map +1 -1
  17. package/dist/modules/camera.d.ts +50 -4
  18. package/dist/modules/camera.d.ts.map +1 -1
  19. package/dist/modules/camera.js +20 -5
  20. package/dist/modules/camera.js.map +1 -1
  21. package/dist/modules/events.d.ts +10 -17
  22. package/dist/modules/events.d.ts.map +1 -1
  23. package/dist/modules/events.js +56 -11
  24. package/dist/modules/events.js.map +1 -1
  25. package/dist/modules/glasses.d.ts +8 -0
  26. package/dist/modules/glasses.d.ts.map +1 -1
  27. package/dist/modules/glasses.js +13 -0
  28. package/dist/modules/glasses.js.map +1 -1
  29. package/dist/modules/languages.d.ts +42 -0
  30. package/dist/modules/languages.d.ts.map +1 -0
  31. package/dist/modules/languages.js +57 -0
  32. package/dist/modules/languages.js.map +1 -0
  33. package/dist/modules/mic.d.ts +17 -0
  34. package/dist/modules/mic.d.ts.map +1 -1
  35. package/dist/modules/mic.js +28 -1
  36. package/dist/modules/mic.js.map +1 -1
  37. package/dist/modules/miniapps.d.ts +2 -0
  38. package/dist/modules/miniapps.d.ts.map +1 -1
  39. package/dist/modules/miniapps.js.map +1 -1
  40. package/dist/modules/phone.d.ts +38 -4
  41. package/dist/modules/phone.d.ts.map +1 -1
  42. package/dist/modules/phone.js +22 -6
  43. package/dist/modules/phone.js.map +1 -1
  44. package/dist/modules/speaker.d.ts +12 -0
  45. package/dist/modules/speaker.d.ts.map +1 -1
  46. package/dist/modules/speaker.js +7 -8
  47. package/dist/modules/speaker.js.map +1 -1
  48. package/dist/modules/stream.d.ts +6 -2
  49. package/dist/modules/stream.d.ts.map +1 -1
  50. package/dist/modules/stream.js +1 -0
  51. package/dist/modules/stream.js.map +1 -1
  52. package/dist/modules/system.d.ts +20 -1
  53. package/dist/modules/system.d.ts.map +1 -1
  54. package/dist/modules/system.js +13 -1
  55. package/dist/modules/system.js.map +1 -1
  56. package/dist/modules/transcription.d.ts +35 -8
  57. package/dist/modules/transcription.d.ts.map +1 -1
  58. package/dist/modules/transcription.js +32 -12
  59. package/dist/modules/transcription.js.map +1 -1
  60. package/dist/modules/translation.d.ts +9 -3
  61. package/dist/modules/translation.d.ts.map +1 -1
  62. package/dist/modules/translation.js +5 -3
  63. package/dist/modules/translation.js.map +1 -1
  64. package/dist/protocol.d.ts +24 -2
  65. package/dist/protocol.d.ts.map +1 -1
  66. package/dist/protocol.js +24 -2
  67. package/dist/protocol.js.map +1 -1
  68. package/dist/react/capsuleHeaderLayout.d.ts +3 -0
  69. package/dist/react/capsuleHeaderLayout.d.ts.map +1 -0
  70. package/dist/react/capsuleHeaderLayout.js +5 -0
  71. package/dist/react/capsuleHeaderLayout.js.map +1 -0
  72. package/dist/react/useCapsuleHeaderStyle.d.ts.map +1 -1
  73. package/dist/react/useCapsuleHeaderStyle.js +6 -1
  74. package/dist/react/useCapsuleHeaderStyle.js.map +1 -1
  75. package/dist/session.d.ts +5 -3
  76. package/dist/session.d.ts.map +1 -1
  77. package/dist/session.js +7 -3
  78. package/dist/session.js.map +1 -1
  79. package/dist/sub-path-types.test-d.d.ts +12 -0
  80. package/dist/sub-path-types.test-d.d.ts.map +1 -1
  81. package/dist/sub-path-types.test-d.js.map +1 -1
  82. package/dist/transport/mock.js +5 -0
  83. package/dist/transport/mock.js.map +1 -1
  84. package/package.json +16 -3
  85. package/src/background/index.ts +18 -4
  86. package/src/background/register.ts +10 -3
  87. package/src/hardware.ts +25 -0
  88. package/src/index.ts +23 -9
  89. package/src/modules/camera.ts +75 -13
  90. package/src/modules/events.ts +54 -27
  91. package/src/modules/glasses.ts +14 -0
  92. package/src/modules/languages.ts +72 -0
  93. package/src/modules/mic.ts +30 -1
  94. package/src/modules/miniapps.ts +2 -0
  95. package/src/modules/phone.ts +59 -24
  96. package/src/modules/speaker.ts +21 -2
  97. package/src/modules/stream.ts +7 -2
  98. package/src/modules/system.ts +26 -1
  99. package/src/modules/transcription.ts +63 -16
  100. package/src/modules/translation.ts +22 -8
  101. package/src/protocol.ts +29 -3
  102. package/src/react/capsuleHeaderLayout.ts +11 -0
  103. package/src/react/useCapsuleHeaderStyle.ts +6 -1
  104. package/src/session.ts +19 -7
  105. package/src/sub-path-types.test-d.ts +12 -1
  106. package/src/transport/mock.ts +7 -0
@@ -50,6 +50,17 @@ export interface SpeakOptions {
50
50
  voice_settings?: Record<string, unknown>
51
51
  volume?: number
52
52
  stopOtherAudio?: boolean
53
+ /**
54
+ * Normalize display-oriented markup and symbols into natural spoken words.
55
+ * Defaults to true. Set false when exact characters must reach the TTS engine.
56
+ */
57
+ enableSanitization?: boolean
58
+ /**
59
+ * Force this call to use on-device offline TTS, skipping cloud TTS
60
+ * entirely — even when cloud is connected. Rejects with `TTS_LOCAL_UNAVAILABLE`
61
+ * if the offline model isn't ready instead of falling back to cloud.
62
+ */
63
+ forceLocal?: boolean
53
64
  }
54
65
 
55
66
  export interface SpeakResult {
@@ -244,7 +255,13 @@ export class SpeakerModule {
244
255
  * Rejects with a MiniappRequestError containing a `code` field on cloud-side
245
256
  * TTS failures: `TTS_TEXT_TOO_LONG`, `TTS_INVALID_VOICE`, `TTS_UPSTREAM_ERROR`.
246
257
  */
247
- async speak(text: string, options: SpeakOptions = {}): Promise<SpeakResult> {
258
+ async speak(text: string, options?: SpeakOptions): Promise<SpeakResult>
259
+ async speak(sentences: string[], options?: SpeakOptions): Promise<SpeakResult>
260
+ async speak(text: string | string[], options: SpeakOptions = {}): Promise<SpeakResult> {
261
+ const normalized = Array.isArray(text) ? text.map((sentence) => sentence.trim()).filter(Boolean) : text.trim()
262
+ if ((Array.isArray(normalized) && normalized.length === 0) || normalized === "") {
263
+ throw {code: MiniappErrorCode.INTERNAL, message: "speak requires at least one non-empty sentence"}
264
+ }
248
265
  try {
249
266
  // Like play(): resolves only when TTS playback completes, so opt out of the
250
267
  // default request timeout (long text can outlast it). Settled by the host
@@ -252,11 +269,13 @@ export class SpeakerModule {
252
269
  const result = await this.session.sendRequest<SpeakResult | null>(
253
270
  {
254
271
  type: MiniappRequestType.SPEAK,
255
- text,
272
+ text: normalized,
256
273
  voice_id: options.voice_id,
257
274
  voice_settings: options.voice_settings,
258
275
  volume: options.volume,
259
276
  stopOtherAudio: options.stopOtherAudio ?? false,
277
+ enableSanitization: options.enableSanitization ?? true,
278
+ forceLocal: options.forceLocal ?? false,
260
279
  },
261
280
  {timeoutMs: 0},
262
281
  )
@@ -77,8 +77,12 @@ export interface StartStreamOptions {
77
77
  * and automatic recording.
78
78
  * - "whip": sub-second WebRTC playback (use `webrtcUrl`/WHEP), but no
79
79
  * HLS/DASH and no recording.
80
+ * - "rtmp": RTMPS ingest over TCP 443 → HLS playback. Use when WHIP/SRT
81
+ * UDP is blocked (then play `hlsUrl`, not WHEP).
80
82
  */
81
- ingest?: "srt" | "whip"
83
+ ingest?: "srt" | "whip" | "rtmp"
84
+ /** Optional Bearer token for direct WHIP Authorization (custom authenticated endpoints). */
85
+ authToken?: string
82
86
  }
83
87
 
84
88
  export interface StreamResult {
@@ -98,7 +102,7 @@ export interface StreamResult {
98
102
  webrtcUrl?: string
99
103
  }
100
104
 
101
- /** Live encoder telemetry, when the glasses emit it (no firmware does yet). */
105
+ /** Live encoder and device telemetry emitted periodically by supported glasses firmware. */
102
106
  export interface StreamLiveStats {
103
107
  /** Current encoded video bitrate in bits per second. */
104
108
  bitrate?: number
@@ -141,6 +145,7 @@ export class StreamModule {
141
145
  video: options.video,
142
146
  audio: options.audio,
143
147
  sound: options.sound ?? true,
148
+ ...(options.authToken ? {authToken: options.authToken} : {}),
144
149
  })
145
150
  }
146
151
  return this.session.sendRequest<StreamResult>({
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview SystemModule — OS-level utilities (share, open URL, clipboard, download).
2
+ * @fileoverview SystemModule — OS-level utilities (share, open URL, clipboard, download, QR scan).
3
3
  *
4
4
  * These bridge to native phone capabilities via LocalMiniappRuntime.
5
5
  */
@@ -37,6 +37,15 @@ export interface DownloadResult {
37
37
  cancelled?: boolean
38
38
  }
39
39
 
40
+ export interface ScanQrOptions {
41
+ /** Header title on the phone scanner overlay. */
42
+ title?: string
43
+ /** Hint shown over the camera viewfinder. */
44
+ hint?: string
45
+ }
46
+
47
+ export type ScanQrResult = {data: string; cancelled?: false} | {cancelled: true; data?: undefined}
48
+
40
49
  export class SystemModule {
41
50
  constructor(private readonly session: MiniappSession) {}
42
51
 
@@ -73,4 +82,20 @@ export class SystemModule {
73
82
  })
74
83
  return result ?? { success: false }
75
84
  }
85
+
86
+ /**
87
+ * Open the phone camera QR scanner as a host overlay. Does not unmount the
88
+ * miniapp (so live sessions survive the scan). Resolves with the raw QR
89
+ * payload, or `{cancelled: true}` if the user dismisses the scanner.
90
+ */
91
+ async scanQr(options: ScanQrOptions = {}): Promise<ScanQrResult> {
92
+ const result = await this.session.sendRequest<ScanQrResult>(
93
+ {
94
+ type: MiniappRequestType.SCAN_QR,
95
+ ...options,
96
+ },
97
+ {timeoutMs: 0},
98
+ )
99
+ return result ?? { cancelled: true }
100
+ }
76
101
  }
@@ -21,17 +21,33 @@
21
21
  import {MiniappRequestType, MiniappStreamType} from "../protocol"
22
22
  import {MiniappSession} from "../session"
23
23
  import type {TranscriptionData, UnsubscribeFn} from "./events"
24
+ import {requireLanguageHint, requireTranscriptionLanguage} from "./languages"
25
+ import type {LanguageHint, TranscriptionLanguage} from "./languages"
24
26
 
25
27
  /** Configuration for cloud-side transcription behavior. */
26
28
  export interface TranscriptionConfig {
27
- /** ISO 639-1 language hints to improve detection accuracy (e.g. ["en", "ja"]). */
28
- languageHints?: string[]
29
+ /**
30
+ * Language hints to improve auto-detection accuracy. Bare ISO 639-1 codes
31
+ * from the registry (e.g. ["en", "ja"]); registry BCP-47 tags are accepted
32
+ * and normalized ("en-US" -> "en"). Anything else throws
33
+ * `MiniappValidationError`.
34
+ */
35
+ languageHints?: LanguageHint[]
29
36
  /** Custom vocabulary / boosted terms (e.g. ["MentraOS", "HIPAA"]). */
30
37
  vocabulary?: string[]
31
38
  /** Enable speaker diarisation. Defaults vary by provider. */
32
39
  diarization?: boolean
33
40
  }
34
41
 
42
+ export interface TranscriptionOptions {
43
+ /**
44
+ * Use only on-device STT for this listener; never deliver cloud transcripts.
45
+ * Other listeners on the same stream keep their own routing choice. Requires
46
+ * a downloaded local STT model.
47
+ */
48
+ forceLocal?: boolean
49
+ }
50
+
35
51
  export class TranscriptionModule {
36
52
  private readonly unsubs = new Set<UnsubscribeFn>()
37
53
  private currentConfig: TranscriptionConfig | null = null
@@ -46,9 +62,9 @@ export class TranscriptionModule {
46
62
  * (handlers on `transcription:auto` receive any `transcription:<lang>`
47
63
  * event) preserves existing semantics.
48
64
  */
49
- on(handler: (data: TranscriptionData) => void): UnsubscribeFn {
65
+ on(handler: (data: TranscriptionData) => void, options: TranscriptionOptions = {}): UnsubscribeFn {
50
66
  return this.track(
51
- this.session._subscribe(`${MiniappStreamType.TRANSCRIPTION}:auto`, handler as (data: unknown) => void),
67
+ this.session._subscribe(`${MiniappStreamType.TRANSCRIPTION}:auto`, handler as (data: unknown) => void, options),
52
68
  )
53
69
  }
54
70
 
@@ -56,17 +72,34 @@ export class TranscriptionModule {
56
72
  * Subscribe to transcription for one or more specific languages. Each call
57
73
  * is independent; multiple can be active simultaneously.
58
74
  *
59
- * @param language - BCP-47 tag(s), e.g. `"en-US"` or `["en-US", "es-ES"]`.
75
+ * Languages are validated against the platform registry
76
+ * (`SUPPORTED_TRANSCRIPTION_LANGUAGES`): registry BCP-47 tags pass through,
77
+ * bare registry codes canonicalize ("fr" -> "fr-FR"), anything else throws
78
+ * `MiniappValidationError` immediately — an invalid language must never
79
+ * become a subscription that silently transcribes the wrong thing
80
+ * (OS-1746 / issue 021).
81
+ *
82
+ * @param language - Registry tag(s), e.g. `"en-US"` or `["en-US", "es-ES"]`.
60
83
  * @param handler - Called for every event in any of the listed languages.
61
84
  */
62
- forLanguage(language: string | string[], handler: (data: TranscriptionData) => void): UnsubscribeFn {
63
- const langs = Array.isArray(language) ? language : [language]
85
+ forLanguage(
86
+ language: TranscriptionLanguage | TranscriptionLanguage[],
87
+ handler: (data: TranscriptionData) => void,
88
+ options: TranscriptionOptions = {},
89
+ ): UnsubscribeFn {
90
+ const langs = (Array.isArray(language) ? language : [language]).map((lang) =>
91
+ requireTranscriptionLanguage(lang, "transcription.forLanguage(language)"),
92
+ )
64
93
  if (langs.length === 0) return () => {}
65
94
 
66
95
  const unsubs: UnsubscribeFn[] = []
67
96
  for (const lang of langs) {
68
97
  unsubs.push(
69
- this.session._subscribe(`${MiniappStreamType.TRANSCRIPTION}:${lang}`, handler as (data: unknown) => void),
98
+ this.session._subscribe(
99
+ `${MiniappStreamType.TRANSCRIPTION}:${lang}`,
100
+ handler as (data: unknown) => void,
101
+ options,
102
+ ),
70
103
  )
71
104
  }
72
105
  const combined: UnsubscribeFn = () => {
@@ -83,15 +116,29 @@ export class TranscriptionModule {
83
116
 
84
117
  /**
85
118
  * Apply transcription configuration (language hints, custom vocabulary,
86
- * diarisation toggle). Sent to the cloud immediately. Cached locally so we
87
- * could re-send on reconnect (future).
119
+ * diarisation toggle). Hints are validated against the registry and
120
+ * normalized to bare codes ("en-US" -> "en"); unknown values throw
121
+ * `MiniappValidationError` synchronously.
122
+ *
123
+ * Sent to the phone runtime as a REQUEST (not a one-shot) so the returned
124
+ * promise rejects if the runtime cannot apply it — a config that the
125
+ * platform ignores must be loud, not silent (issue 021 S3). Cached locally
126
+ * so the runtime can re-apply on reconnect.
88
127
  */
89
- configure(config: TranscriptionConfig): void {
90
- this.currentConfig = {...config}
91
- this.session.sendOneShot({
92
- type: MiniappRequestType.TRANSCRIPTION_CONFIG,
93
- config: {...config},
94
- })
128
+ configure(config: TranscriptionConfig): Promise<void> {
129
+ const normalized: TranscriptionConfig = {
130
+ ...config,
131
+ languageHints: config.languageHints?.map((hint) =>
132
+ requireLanguageHint(hint, "transcription.configure(languageHints)"),
133
+ ),
134
+ }
135
+ this.currentConfig = {...normalized}
136
+ return this.session
137
+ .sendRequest<void>({
138
+ type: MiniappRequestType.TRANSCRIPTION_CONFIG,
139
+ config: {...normalized},
140
+ })
141
+ .then(() => undefined)
95
142
  }
96
143
 
97
144
  /** Tear down every transcription subscription this module owns. */
@@ -20,9 +20,17 @@
20
20
  import {MiniappStreamType} from "../protocol"
21
21
  import {MiniappSession} from "../session"
22
22
  import type {TranslationData, UnsubscribeFn} from "./events"
23
+ import {requireTranscriptionLanguage} from "./languages"
24
+ import type {TranscriptionLanguage} from "./languages"
23
25
 
24
26
  export type TranslationHandler = (data: TranslationData) => void
25
27
 
28
+ /**
29
+ * Translation source: a registry language, or "auto" to let the cloud detect
30
+ * the spoken language.
31
+ */
32
+ export type TranslationSource = TranscriptionLanguage | "auto"
33
+
26
34
  export class TranslationModule {
27
35
  private readonly unsubs = new Set<UnsubscribeFn>()
28
36
 
@@ -51,8 +59,10 @@ export class TranslationModule {
51
59
  * regardless of source. Useful for "I'm a Spanish speaker, translate
52
60
  * whatever I hear to Spanish."
53
61
  */
54
- to(target: string | string[], handler: TranslationHandler): UnsubscribeFn {
55
- const targets = Array.isArray(target) ? target : [target]
62
+ to(target: TranscriptionLanguage | TranscriptionLanguage[], handler: TranslationHandler): UnsubscribeFn {
63
+ const targets = (Array.isArray(target) ? target : [target]).map((t) =>
64
+ requireTranscriptionLanguage(t, "translation.to(target)"),
65
+ )
56
66
  if (targets.length === 0) return () => {}
57
67
  const unsubs: UnsubscribeFn[] = []
58
68
  for (const t of targets) {
@@ -81,17 +91,21 @@ export class TranslationModule {
81
91
  * from the same source.
82
92
  */
83
93
  fromTo(
84
- source: string,
85
- target: string | string[],
94
+ source: TranslationSource,
95
+ target: TranscriptionLanguage | TranscriptionLanguage[],
86
96
  handler: TranslationHandler,
87
97
  ): UnsubscribeFn {
88
- const targets = Array.isArray(target) ? target : [target]
98
+ const validSource =
99
+ source === "auto" ? "auto" : requireTranscriptionLanguage(source, "translation.fromTo(source)")
100
+ const targets = (Array.isArray(target) ? target : [target]).map((t) =>
101
+ requireTranscriptionLanguage(t, "translation.fromTo(target)"),
102
+ )
89
103
  if (targets.length === 0) return () => {}
90
104
  const unsubs: UnsubscribeFn[] = []
91
105
  for (const t of targets) {
92
106
  unsubs.push(
93
107
  this.session._subscribe(
94
- `${MiniappStreamType.TRANSLATION}:${source}:${t}`,
108
+ `${MiniappStreamType.TRANSLATION}:${validSource}:${t}`,
95
109
  handler as (data: unknown) => void,
96
110
  ),
97
111
  )
@@ -113,8 +127,8 @@ export class TranslationModule {
113
127
  * SDK v3 parity. This alias will be removed in a future release.
114
128
  */
115
129
  forLanguagePair(
116
- fromLang: string,
117
- toLang: string,
130
+ fromLang: TranslationSource,
131
+ toLang: TranscriptionLanguage,
118
132
  handler: TranslationHandler,
119
133
  ): UnsubscribeFn {
120
134
  return this.fromTo(fromLang, toLang, handler)
package/src/protocol.ts CHANGED
@@ -60,6 +60,9 @@ export enum MiniappRequestType {
60
60
  /** One-shot location poll. */
61
61
  LOCATION_POLL = "miniapp_location_poll",
62
62
 
63
+ /** Read a bounded snapshot of phone calendar events. */
64
+ CALENDAR_LIST_EVENTS = "miniapp_calendar_list_events",
65
+
63
66
  /** Start a turn-by-turn navigation trip. Android only. */
64
67
  NAVIGATION_START = "miniapp_navigation_start",
65
68
  /** Stop the active navigation trip (if any). */
@@ -114,6 +117,17 @@ export enum MiniappRequestType {
114
117
  */
115
118
  IMU_SET_ENABLED = "miniapp_imu_set_enabled",
116
119
 
120
+ /** Explicitly enable/disable glasses-side VAD (GX8002). Mirrors the app's mic settings toggle. */
121
+ MIC_SET_VAD_ENABLED = "miniapp_mic_set_vad_enabled",
122
+ /** Explicitly enable/disable the center-mic loudness gate ("Barrier"). */
123
+ MIC_SET_LOUDNESS_GATE_ENABLED = "miniapp_mic_set_loudness_gate_enabled",
124
+
125
+ /**
126
+ * Enable or disable Wi-Fi ADB (wireless debugging) on Mentra Live.
127
+ * SYSTEM-only — host rejects with NOT_PERMITTED for non-system callers.
128
+ */
129
+ SET_WIFI_ADB_STATE = "miniapp_set_wifi_adb_state",
130
+
117
131
  /** Share content via the OS share sheet. */
118
132
  SHARE = "miniapp_share",
119
133
  /** Open a URL in the system browser. */
@@ -122,6 +136,11 @@ export enum MiniappRequestType {
122
136
  COPY_CLIPBOARD = "miniapp_copy_clipboard",
123
137
  /** Download a file (triggers OS share sheet for save location). */
124
138
  DOWNLOAD = "miniapp_download",
139
+ /**
140
+ * Open a phone-camera QR scanner overlay. Host must not unmount the miniapp
141
+ * (clearing foreground fires UI_CLOSE and can hang up live sessions).
142
+ */
143
+ SCAN_QR = "miniapp_scan_qr",
125
144
 
126
145
  /**
127
146
  * Phone-local persistent binary blob storage, scoped to (userId, packageName).
@@ -300,8 +319,6 @@ export enum MiniappStreamType {
300
319
  * fire. See agents/miniapp-speaker-state-and-notif-dismissed-plan.md.
301
320
  */
302
321
  PHONE_NOTIFICATION_DISMISSED = "phone_notification_dismissed",
303
- CALENDAR_EVENT = "calendar_event",
304
-
305
322
  // Photos, streaming
306
323
  PHOTO_TAKEN = "photo_taken",
307
324
  STREAM_STATUS = "stream_status",
@@ -318,6 +335,9 @@ export enum MiniappErrorCode {
318
335
  /** The miniapp subscribed to a stream whose required permission wasn't in its manifest. */
319
336
  PERMISSION_NOT_DECLARED = "PERMISSION_NOT_DECLARED",
320
337
 
338
+ /** The required phone OS permission is not currently granted. */
339
+ PERMISSION_DENIED = "PERMISSION_DENIED",
340
+
321
341
  /** Request routed to a method that isn't supported yet. */
322
342
  NOT_IMPLEMENTED = "NOT_IMPLEMENTED",
323
343
 
@@ -332,11 +352,17 @@ export enum MiniappErrorCode {
332
352
  TTS_INVALID_VOICE = "TTS_INVALID_VOICE",
333
353
  TTS_UPSTREAM_ERROR = "TTS_UPSTREAM_ERROR",
334
354
 
355
+ /** `speak({forceLocal: true})` but the on-device offline TTS model isn't downloaded/ready. */
356
+ TTS_LOCAL_UNAVAILABLE = "TTS_LOCAL_UNAVAILABLE",
357
+
335
358
  /** Not connected / pre-ACK and transport closed. */
336
359
  NOT_CONNECTED = "NOT_CONNECTED",
337
360
 
338
361
  // ----- Inter-miniapp interop -----
339
- /** Caller is not a system app — interop APIs (list/start/stop/invoke) are SYSTEM-only. */
362
+ /**
363
+ * Caller is not a system app — SYSTEM-only APIs (interop list/start/stop/invoke,
364
+ * setWifiAdbState) reject with this code.
365
+ */
340
366
  NOT_PERMITTED = "NOT_PERMITTED",
341
367
  /** Target miniapp is not installed. */
342
368
  APP_NOT_FOUND = "APP_NOT_FOUND",
@@ -0,0 +1,11 @@
1
+ import type {MiniappCapsuleMenuRect} from "../globals"
2
+
3
+ export function getCapsuleHeaderPaddingRight(
4
+ capsuleMenu: MiniappCapsuleMenuRect,
5
+ viewportWidth: number,
6
+ safeAreaRight: number,
7
+ rightGap: number,
8
+ ): number {
9
+ const capsuleOffsetFromContentRight = viewportWidth - capsuleMenu.left - safeAreaRight
10
+ return Math.max(rightGap, capsuleOffsetFromContentRight + rightGap)
11
+ }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import type {CSSProperties} from "react"
17
17
 
18
+ import {getCapsuleHeaderPaddingRight} from "./capsuleHeaderLayout"
18
19
  import {useSafeArea} from "./useSafeArea"
19
20
 
20
21
  export interface UseCapsuleHeaderStyleOptions {
@@ -39,7 +40,11 @@ export function useCapsuleHeaderStyle(
39
40
  ? capsuleMenu.top + capsuleMenu.height / 2 - insets.top
40
41
  : fallbackMarginTop + height / 2
41
42
  const marginTop = Math.max(0, center - height / 2)
42
- const paddingRight = capsuleMenu ? capsuleMenu.width + rightGap : leftPadding
43
+ const paddingRight = capsuleMenu
44
+ ? typeof window === "undefined"
45
+ ? capsuleMenu.width + rightGap
46
+ : getCapsuleHeaderPaddingRight(capsuleMenu, window.innerWidth, insets.right, rightGap)
47
+ : leftPadding
43
48
 
44
49
  return {
45
50
  display: "flex",
package/src/session.ts CHANGED
@@ -24,7 +24,7 @@ import {AuthModule} from "./modules/auth"
24
24
  import {CloudModule} from "./modules/cloud"
25
25
  import {DashboardAPI} from "./modules/dashboard"
26
26
  import {DisplayManager} from "./modules/display"
27
- import {EventManager, type UnsubscribeFn} from "./modules/events"
27
+ import {EventManager, type TranscriptionEventRoute, type UnsubscribeFn} from "./modules/events"
28
28
  import {GlassesModule} from "./modules/glasses"
29
29
  import {HeadingModule} from "./modules/heading"
30
30
  import {ImuModule} from "./modules/imu"
@@ -194,7 +194,11 @@ type SessionEmitterEvents = {
194
194
  auth: (auth: MiniappAuthState) => void
195
195
  }
196
196
 
197
- export class MiniappSession {
197
+ // The default preserves the pre-channel-registry behavior for code that creates
198
+ // or accepts a bare MiniappSession. `registerMiniapp<Channels>` supplies the
199
+ // concrete mapping for scaffolded miniapps.
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ export class MiniappSession<TChannels extends object = any> {
198
202
  public readonly auth: AuthModule
199
203
  public readonly display: DisplayManager
200
204
  /**
@@ -238,7 +242,7 @@ export class MiniappSession {
238
242
  * with inverted buffering policy (background drops when no WebView is
239
243
  * bound; the WebView buffers until ready).
240
244
  */
241
- public readonly ui: UIModule
245
+ public readonly ui: UIModule<TChannels>
242
246
  /**
243
247
  * Inter-miniapp lifecycle + discovery (list / start / stop). SYSTEM-only —
244
248
  * calls reject with NOT_PERMITTED unless this miniapp is a system app.
@@ -324,7 +328,7 @@ export class MiniappSession {
324
328
  this.system = new SystemModule(this)
325
329
  this.transcription = new TranscriptionModule(this)
326
330
  this.translation = new TranslationModule(this)
327
- this.ui = new UIModuleImpl(this)
331
+ this.ui = new UIModuleImpl<TChannels>(this)
328
332
  this.miniapps = new MiniappsModule(this)
329
333
  this.actions = new ActionsModule(this)
330
334
  }
@@ -392,8 +396,12 @@ export class MiniappSession {
392
396
  * session.transcription.on(...)
393
397
  * etc. instead."
394
398
  */
395
- _subscribe(streamType: string, handler: (data: unknown) => void): UnsubscribeFn {
396
- return this.events.subscribe(streamType, handler)
399
+ _subscribe(
400
+ streamType: string,
401
+ handler: (data: unknown) => void,
402
+ options: {forceLocal?: boolean} = {},
403
+ ): UnsubscribeFn {
404
+ return this.events.subscribe(streamType, handler, options)
397
405
  }
398
406
 
399
407
  // -------------------------------------------------------------------------
@@ -677,7 +685,11 @@ export class MiniappSession {
677
685
  case MiniappResponseType.EVENT: {
678
686
  const streamType = payload.streamType as string | undefined
679
687
  if (!streamType) return
680
- this.events._forwardEvent(streamType, payload.data)
688
+ this.events._forwardEvent(
689
+ streamType,
690
+ payload.data,
691
+ payload.transcriptionRoute as TranscriptionEventRoute | undefined,
692
+ )
681
693
  return
682
694
  }
683
695
 
@@ -23,12 +23,23 @@
23
23
  */
24
24
 
25
25
  // ----- @mentra/miniapp/background ---------------------------------------
26
- import type {MiniappSession} from "./background/index"
26
+ import {registerMiniapp} from "./background/index"
27
+ import type {MiniappSession, UIModule} from "./background/index"
27
28
 
28
29
  // ✅ MiniappSession is reachable from the background entry.
29
30
  const _bg: MiniappSession | undefined = undefined
30
31
  void _bg
31
32
 
33
+ // ✅ registerMiniapp<Channels> carries the shared channel registry into
34
+ // session.ui on the background side.
35
+ interface _TestChannels {
36
+ ping: {at: number}
37
+ }
38
+ type _RegisteredHandler = Parameters<typeof registerMiniapp<_TestChannels>>[0]
39
+ type _RegisteredSession = Parameters<_RegisteredHandler>[0]
40
+ type _Assert<T extends true> = T
41
+ export type _TypedBackground = _Assert<_RegisteredSession["ui"] extends UIModule<_TestChannels> ? true : false>
42
+
32
43
  // ----- @mentra/miniapp/ui -----------------------------------------------
33
44
  import type {MentraUiGlobal, MentraTyped} from "./ui/index"
34
45
 
@@ -226,6 +226,9 @@ function syntheticDataFor(requestId: string, requestType: string, requestPayload
226
226
  // Atlantic, which makes maps/navigation unusable in the WebView.
227
227
  return {lat: 37.7956, lng: -122.3933, accuracy: 0, timestamp: Date.now()}
228
228
 
229
+ case MiniappRequestType.CALENDAR_LIST_EVENTS:
230
+ return {events: [], truncated: false}
231
+
229
232
  case MiniappRequestType.STORAGE_GET:
230
233
  return {value: null}
231
234
 
@@ -240,8 +243,12 @@ function syntheticDataFor(requestId: string, requestType: string, requestPayload
240
243
  case MiniappRequestType.COPY_CLIPBOARD:
241
244
  case MiniappRequestType.DOWNLOAD:
242
245
  case MiniappRequestType.REQUEST_WIFI_SETUP:
246
+ case MiniappRequestType.SET_WIFI_ADB_STATE:
243
247
  return {ok: true}
244
248
 
249
+ case MiniappRequestType.SCAN_QR:
250
+ return {cancelled: true}
251
+
245
252
  default:
246
253
  return null
247
254
  }