@glassly/engine 0.4.0 → 0.4.1-dev.1137

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 (230) hide show
  1. package/build/engine.d.ts +3 -1
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +6 -4
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/display.d.ts +1 -1
  6. package/build/facades/display.d.ts.map +1 -1
  7. package/build/facades/display.js +1 -1
  8. package/build/facades/display.js.map +1 -1
  9. package/build/facades/glasses.js +1 -1
  10. package/build/facades/glasses.js.map +1 -1
  11. package/build/facades/permissions.d.ts.map +1 -1
  12. package/build/facades/permissions.js +0 -2
  13. package/build/facades/permissions.js.map +1 -1
  14. package/build/facades/phoneNotifications.d.ts +7 -0
  15. package/build/facades/phoneNotifications.d.ts.map +1 -1
  16. package/build/facades/phoneNotifications.js +6 -0
  17. package/build/facades/phoneNotifications.js.map +1 -1
  18. package/build/facades/providerKeys.d.ts +1 -1
  19. package/build/facades/providerKeys.js +1 -1
  20. package/build/facades/providerKeys.js.map +1 -1
  21. package/build/facades/reports.d.ts +15 -0
  22. package/build/facades/reports.d.ts.map +1 -1
  23. package/build/facades/reports.js +37 -0
  24. package/build/facades/reports.js.map +1 -1
  25. package/build/facades/settings.js +1 -1
  26. package/build/facades/settings.js.map +1 -1
  27. package/build/generated/releaseMetadata.js +4 -4
  28. package/build/generated/releaseMetadata.js.map +1 -1
  29. package/build/index.d.ts +2 -1
  30. package/build/index.d.ts.map +1 -1
  31. package/build/index.js +1 -1
  32. package/build/index.js.map +1 -1
  33. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  34. package/build/react/useMentraLiveOta.js.map +1 -1
  35. package/build/runtime/config.d.ts.map +1 -1
  36. package/build/runtime/config.js.map +1 -1
  37. package/build/services/AiTierResolution.d.ts +1 -1
  38. package/build/services/AiTierResolution.d.ts.map +1 -1
  39. package/build/services/AiTierResolution.js.map +1 -1
  40. package/build/services/AppRegistry.d.ts +12 -1
  41. package/build/services/AppRegistry.d.ts.map +1 -1
  42. package/build/services/AppRegistry.js +33 -0
  43. package/build/services/AppRegistry.js.map +1 -1
  44. package/build/services/AutomaticReportResult.d.ts.map +1 -1
  45. package/build/services/AutomaticReportResult.js.map +1 -1
  46. package/build/services/CloudClientService.d.ts +7 -5
  47. package/build/services/CloudClientService.d.ts.map +1 -1
  48. package/build/services/CloudClientService.js +17 -14
  49. package/build/services/CloudClientService.js.map +1 -1
  50. package/build/services/DisplayProcessor.d.ts.map +1 -1
  51. package/build/services/DisplayProcessor.js +2 -1
  52. package/build/services/DisplayProcessor.js.map +1 -1
  53. package/build/services/GestureDotsService.js +1 -1
  54. package/build/services/GestureDotsService.js.map +1 -1
  55. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  56. package/build/services/HotspotOtaTransport.js +0 -1
  57. package/build/services/HotspotOtaTransport.js.map +1 -1
  58. package/build/services/JSRouter.js +2 -2
  59. package/build/services/JSRouter.js.map +1 -1
  60. package/build/services/LocalLlmModelManager.d.ts.map +1 -1
  61. package/build/services/LocalLlmModelManager.js +4 -3
  62. package/build/services/LocalLlmModelManager.js.map +1 -1
  63. package/build/services/LocalLlmProvider.d.ts.map +1 -1
  64. package/build/services/LocalLlmProvider.js +5 -6
  65. package/build/services/LocalLlmProvider.js.map +1 -1
  66. package/build/services/LocalMiniappRuntime.d.ts +10 -3
  67. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  68. package/build/services/LocalMiniappRuntime.js +142 -30
  69. package/build/services/LocalMiniappRuntime.js.map +1 -1
  70. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  71. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  72. package/build/services/NativeNavigationService.d.ts.map +1 -1
  73. package/build/services/NativeNavigationService.js.map +1 -1
  74. package/build/services/NavigationHandlers.d.ts +1 -1
  75. package/build/services/NavigationHandlers.js +2 -2
  76. package/build/services/NavigationHandlers.js.map +1 -1
  77. package/build/services/NavigationService.d.ts +2 -2
  78. package/build/services/NavigationService.d.ts.map +1 -1
  79. package/build/services/NavigationService.js +11 -11
  80. package/build/services/NavigationService.js.map +1 -1
  81. package/build/services/PairingIdentity.d.ts.map +1 -1
  82. package/build/services/PairingIdentity.js.map +1 -1
  83. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  84. package/build/services/PhoneCalendarService.js.map +1 -1
  85. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  86. package/build/services/PhoneNotificationsSync.js +2 -1
  87. package/build/services/PhoneNotificationsSync.js.map +1 -1
  88. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  89. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  90. package/build/services/ProviderKeyValidation.d.ts +1 -1
  91. package/build/services/ProviderKeyValidation.js +1 -1
  92. package/build/services/ProviderKeyValidation.js.map +1 -1
  93. package/build/services/STTModelManager.d.ts.map +1 -1
  94. package/build/services/STTModelManager.js +1 -6
  95. package/build/services/STTModelManager.js.map +1 -1
  96. package/build/services/SupportProfileSync.d.ts.map +1 -1
  97. package/build/services/SupportProfileSync.js +3 -0
  98. package/build/services/SupportProfileSync.js.map +1 -1
  99. package/build/services/TranscriptionRouting.d.ts +3 -0
  100. package/build/services/TranscriptionRouting.d.ts.map +1 -1
  101. package/build/services/TranscriptionRouting.js +14 -0
  102. package/build/services/TranscriptionRouting.js.map +1 -1
  103. package/build/services/UIRouter.d.ts.map +1 -1
  104. package/build/services/UIRouter.js.map +1 -1
  105. package/build/services/controllers.d.ts.map +1 -1
  106. package/build/services/controllers.js +1 -0
  107. package/build/services/controllers.js.map +1 -1
  108. package/build/services/effectiveCapabilities.js +3 -2
  109. package/build/services/effectiveCapabilities.js.map +1 -1
  110. package/build/services/gestureDotGlyphs.d.ts.map +1 -1
  111. package/build/services/gestureDotGlyphs.js.map +1 -1
  112. package/build/services/manifestActions.d.ts.map +1 -1
  113. package/build/services/manifestActions.js.map +1 -1
  114. package/build/services/neuralBand/MetaAuth.d.ts +1 -0
  115. package/build/services/neuralBand/MetaAuth.d.ts.map +1 -1
  116. package/build/services/neuralBand/MetaAuth.js +36 -8
  117. package/build/services/neuralBand/MetaAuth.js.map +1 -1
  118. package/build/services/notificationAppPayload.d.ts +14 -0
  119. package/build/services/notificationAppPayload.d.ts.map +1 -0
  120. package/build/services/notificationAppPayload.js +15 -0
  121. package/build/services/notificationAppPayload.js.map +1 -0
  122. package/build/services/notifications/miniappNotifications.d.ts +31 -0
  123. package/build/services/notifications/miniappNotifications.d.ts.map +1 -0
  124. package/build/services/notifications/miniappNotifications.js +24 -0
  125. package/build/services/notifications/miniappNotifications.js.map +1 -0
  126. package/build/services/pages/pageEligibility.d.ts +1 -2
  127. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  128. package/build/services/pages/pageEligibility.js +4 -2
  129. package/build/services/pages/pageEligibility.js.map +1 -1
  130. package/build/services/settingsBridge.d.ts.map +1 -1
  131. package/build/services/settingsBridge.js +0 -2
  132. package/build/services/settingsBridge.js.map +1 -1
  133. package/build/services/uiShim.d.ts.map +1 -1
  134. package/build/services/uiShim.js +14 -0
  135. package/build/services/uiShim.js.map +1 -1
  136. package/build/stores/appVisibility.d.ts.map +1 -1
  137. package/build/stores/appVisibility.js.map +1 -1
  138. package/build/stores/gallerySync.d.ts.map +1 -1
  139. package/build/stores/gallerySync.js.map +1 -1
  140. package/build/stores/glasses.d.ts.map +1 -1
  141. package/build/stores/glasses.js +1 -0
  142. package/build/stores/glasses.js.map +1 -1
  143. package/build/stores/settings.d.ts.map +1 -1
  144. package/build/stores/settings.js +27 -28
  145. package/build/stores/settings.js.map +1 -1
  146. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  147. package/build/types/capabilities/even-realities-g1.js +4 -0
  148. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  149. package/build/types/capabilities/nimo.d.ts.map +1 -1
  150. package/build/types/capabilities/nimo.js +21 -7
  151. package/build/types/capabilities/nimo.js.map +1 -1
  152. package/build/types/enums.d.ts +2 -1
  153. package/build/types/enums.d.ts.map +1 -1
  154. package/build/types/enums.js +1 -0
  155. package/build/types/enums.js.map +1 -1
  156. package/build/utils/display/index.d.ts +1 -0
  157. package/build/utils/display/index.d.ts.map +1 -1
  158. package/build/utils/display/index.js +2 -2
  159. package/build/utils/display/index.js.map +1 -1
  160. package/build/utils/display/profiles/index.d.ts +1 -0
  161. package/build/utils/display/profiles/index.d.ts.map +1 -1
  162. package/build/utils/display/profiles/index.js +1 -0
  163. package/build/utils/display/profiles/index.js.map +1 -1
  164. package/build/utils/display/profiles/nimo.d.ts +2 -0
  165. package/build/utils/display/profiles/nimo.d.ts.map +1 -0
  166. package/build/utils/display/profiles/nimo.js +3 -0
  167. package/build/utils/display/profiles/nimo.js.map +1 -0
  168. package/build/utils/display/scene/svgPath.js.map +1 -1
  169. package/build/utils/memory/search.d.ts.map +1 -1
  170. package/build/utils/memory/search.js +17 -7
  171. package/build/utils/memory/search.js.map +1 -1
  172. package/package.json +15 -15
  173. package/src/engine.ts +9 -7
  174. package/src/facades/display.ts +1 -1
  175. package/src/facades/glasses.ts +1 -1
  176. package/src/facades/permissions.ts +0 -2
  177. package/src/facades/phoneNotifications.ts +10 -0
  178. package/src/facades/providerKeys.ts +1 -1
  179. package/src/facades/reports.ts +41 -0
  180. package/src/facades/settings.ts +1 -1
  181. package/src/generated/releaseMetadata.ts +4 -4
  182. package/src/index.ts +2 -0
  183. package/src/react/useMentraLiveOta.ts +1 -4
  184. package/src/runtime/config.ts +1 -6
  185. package/src/services/AiTierResolution.ts +1 -1
  186. package/src/services/AppRegistry.ts +38 -1
  187. package/src/services/AutomaticReportResult.ts +1 -3
  188. package/src/services/CloudClientService.ts +17 -11
  189. package/src/services/DisplayProcessor.ts +2 -0
  190. package/src/services/GestureDotsService.ts +1 -1
  191. package/src/services/HotspotOtaTransport.ts +1 -5
  192. package/src/services/JSRouter.ts +2 -2
  193. package/src/services/LocalLlmModelManager.ts +4 -3
  194. package/src/services/LocalLlmProvider.ts +6 -7
  195. package/src/services/LocalMiniappRuntime.ts +170 -40
  196. package/src/services/LocalSttFallbackCoordinator.ts +1 -5
  197. package/src/services/NativeNavigationService.ts +1 -2
  198. package/src/services/NavigationHandlers.ts +2 -2
  199. package/src/services/NavigationService.ts +12 -11
  200. package/src/services/PairingIdentity.ts +1 -3
  201. package/src/services/PhoneCalendarService.ts +1 -3
  202. package/src/services/PhoneNotificationsSync.ts +2 -1
  203. package/src/services/PhoneStreamCoordinator.ts +1 -2
  204. package/src/services/ProviderKeyValidation.ts +1 -1
  205. package/src/services/STTModelManager.ts +1 -6
  206. package/src/services/SupportProfileSync.ts +2 -0
  207. package/src/services/TranscriptionRouting.ts +18 -0
  208. package/src/services/UIRouter.ts +3 -9
  209. package/src/services/controllers.ts +1 -0
  210. package/src/services/effectiveCapabilities.ts +3 -2
  211. package/src/services/gestureDotGlyphs.ts +10 -5
  212. package/src/services/manifestActions.ts +2 -1
  213. package/src/services/neuralBand/MetaAuth.ts +37 -8
  214. package/src/services/notificationAppPayload.ts +15 -0
  215. package/src/services/notifications/miniappNotifications.ts +53 -0
  216. package/src/services/pages/pageEligibility.ts +3 -2
  217. package/src/services/settingsBridge.ts +1 -4
  218. package/src/services/uiShim.ts +14 -0
  219. package/src/stores/appVisibility.ts +2 -5
  220. package/src/stores/gallerySync.ts +1 -7
  221. package/src/stores/glasses.ts +1 -0
  222. package/src/stores/settings.ts +27 -28
  223. package/src/types/capabilities/even-realities-g1.ts +5 -0
  224. package/src/types/capabilities/nimo.ts +23 -7
  225. package/src/types/enums.ts +1 -0
  226. package/src/utils/display/index.ts +2 -2
  227. package/src/utils/display/profiles/index.ts +2 -0
  228. package/src/utils/display/profiles/nimo.ts +2 -0
  229. package/src/utils/display/scene/svgPath.ts +1 -1
  230. package/src/utils/memory/search.ts +17 -7
@@ -14,6 +14,7 @@ import {AppState, Linking} from "react-native"
14
14
  import {logBuffer} from "../utils/devLogging"
15
15
  import {collectDisplayDiagnostics} from "./DisplayDiagnostics"
16
16
  import Share from "react-native-share"
17
+ import {uses24HourClock} from "react-native-localize"
17
18
  import * as Battery from "expo-battery"
18
19
  import * as Clipboard from "expo-clipboard"
19
20
  import {File, Paths} from "expo-file-system"
@@ -83,6 +84,13 @@ import overlayPages from "./pages/OverlayPages"
83
84
  import dashboardOpenTrace from "./debug/DashboardOpenTrace"
84
85
  import {isAppForegrounded, isPhoneActive, onAppForegroundChange, onPhoneActivityChange} from "./PhoneActivity"
85
86
  import {installedNotificationApps} from "./PhoneNotificationsSync"
87
+ import {
88
+ emitMiniappNotification,
89
+ MINIAPP_NOTIFICATION_CONTENT_MAX,
90
+ MINIAPP_NOTIFICATION_ID_MAX,
91
+ MINIAPP_NOTIFICATION_TITLE_MAX,
92
+ miniappNotificationId,
93
+ } from "./notifications/miniappNotifications"
86
94
  import {MiniappIcons} from "./MiniappIcons"
87
95
  import {pairedControllersFrom, readyControllersKey} from "./controllers"
88
96
  import {
@@ -141,6 +149,7 @@ import {phoneStreamCoordinator} from "./PhoneStreamCoordinator"
141
149
  import {phoneVideoCoordinator} from "./PhoneVideoCoordinator"
142
150
  import {runSentenceTtsPipeline} from "./SentenceTtsPipeline"
143
151
  import {
152
+ matchesTranscriptionSubscription,
144
153
  summarizeTranscriptionRoutes,
145
154
  transcriptionDeliveryRoute,
146
155
  type TranscriptionDeliveryRoute,
@@ -433,7 +442,7 @@ const PERMISSION_TYPE_TO_CANONICAL: Record<string, string> = {
433
442
  }
434
443
 
435
444
  /**
436
- * User API keys (Settings > API Keys) a miniapp may read via
445
+ * User API keys (Settings > Privacy > API Keys) a miniapp may read via
437
446
  * session.keys.get(), mapped provider → settings-store key. Explicit
438
447
  * allowlist: the Soniox key is deliberately absent — it gates the
439
448
  * phone→Soniox direct transcription path and is not exposed to miniapps.
@@ -477,9 +486,9 @@ function directLlmProvider(name: AiTierProvider): LlmProviderImpl {
477
486
  }
478
487
 
479
488
  const QUOTA_EXCEEDED_MESSAGE =
480
- "AI credits are used up. Get more in Settings > Membership, or add your own API key in Settings > API Keys."
489
+ "AI credits are used up. Get more in Settings > Membership, or add your own API key in Settings > Privacy > API Keys."
481
490
 
482
- /** The user's saved key (Settings > API Keys) for an LLM provider, "" if none. */
491
+ /** The user's saved key (Settings > Privacy > API Keys) for an LLM provider, "" if none. */
483
492
  function userLlmKey(provider: AiTierProvider): string {
484
493
  const raw = useSettingsStore.getState().getSetting(MINIAPP_READABLE_API_KEYS[provider])
485
494
  return typeof raw === "string" ? raw.trim() : ""
@@ -1512,6 +1521,12 @@ class LocalMiniappRuntime {
1512
1521
  case MiniappRequestType.NOTIFICATION_APPS_LIST:
1513
1522
  void this.handleNotificationAppsList(packageName, requestId)
1514
1523
  break
1524
+ case MiniappRequestType.NOTIFICATION_POST:
1525
+ this.handleNotificationPost(packageName, payload, requestId)
1526
+ break
1527
+ case MiniappRequestType.NOTIFICATION_DISMISS:
1528
+ this.handleNotificationDismiss(packageName, payload, requestId)
1529
+ break
1515
1530
  case MiniappRequestType.NATIVE_NAV:
1516
1531
  this.handleNativeNav(packageName, payload, requestId)
1517
1532
  break
@@ -2791,7 +2806,17 @@ class LocalMiniappRuntime {
2791
2806
  return
2792
2807
  }
2793
2808
  try {
2794
- this.sendResult(packageName, requestId, true, await installedNotificationApps())
2809
+ const posters = this.interopApps().filter(
2810
+ (app) => app.packageName && app.permissions?.some((permission) => permission.type === "POST_NOTIFICATIONS"),
2811
+ )
2812
+ const miniapps = await Promise.all(
2813
+ posters.map(async (app) => ({
2814
+ packageName: app.packageName,
2815
+ appName: app.name || app.packageName,
2816
+ icon: (await this.miniappIcons.read(app)) ?? null,
2817
+ })),
2818
+ )
2819
+ this.sendResult(packageName, requestId, true, [...(await installedNotificationApps()), ...miniapps])
2795
2820
  } catch (err) {
2796
2821
  this.sendResult(packageName, requestId, false, undefined, {
2797
2822
  code: MiniappErrorCode.INTERNAL,
@@ -2800,6 +2825,60 @@ class LocalMiniappRuntime {
2800
2825
  }
2801
2826
  }
2802
2827
 
2828
+ private handleNotificationPost(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2829
+ if (!this.requirePermission(packageName, requestId, "POST_NOTIFICATIONS", "to post notifications")) return
2830
+ const title = typeof payload.title === "string" ? payload.title.slice(0, MINIAPP_NOTIFICATION_TITLE_MAX) : ""
2831
+ const content =
2832
+ typeof payload.content === "string" ? payload.content.slice(0, MINIAPP_NOTIFICATION_CONTENT_MAX) : ""
2833
+ if (!title.trim() && !content.trim()) {
2834
+ this.sendResult(packageName, requestId, false, undefined, {
2835
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2836
+ message: "A notification needs a title or content",
2837
+ })
2838
+ return
2839
+ }
2840
+ if (payload.id !== undefined && (typeof payload.id !== "string" || !payload.id.trim())) {
2841
+ this.sendResult(packageName, requestId, false, undefined, {
2842
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2843
+ message: "A notification id must be a non-empty string",
2844
+ })
2845
+ return
2846
+ }
2847
+ const id =
2848
+ typeof payload.id === "string"
2849
+ ? payload.id.trim().slice(0, MINIAPP_NOTIFICATION_ID_MAX)
2850
+ : `${Date.now().toString(36)}-${++this.notificationSeq}`
2851
+ const app = this.interopApps().find((candidate) => candidate.packageName === packageName)
2852
+ emitMiniappNotification({
2853
+ type: "posted",
2854
+ notificationId: miniappNotificationId(packageName, id),
2855
+ source: packageName,
2856
+ app: app?.name || packageName,
2857
+ title,
2858
+ content,
2859
+ timestamp: Date.now(),
2860
+ })
2861
+ this.sendResult(packageName, requestId, true, {id})
2862
+ }
2863
+
2864
+ private handleNotificationDismiss(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2865
+ if (!this.requirePermission(packageName, requestId, "POST_NOTIFICATIONS", "to dismiss notifications")) return
2866
+ if (typeof payload.id !== "string" || !payload.id.trim()) {
2867
+ this.sendResult(packageName, requestId, false, undefined, {
2868
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2869
+ message: "dismiss needs the id post() returned",
2870
+ })
2871
+ return
2872
+ }
2873
+ emitMiniappNotification({
2874
+ type: "dismissed",
2875
+ notificationId: miniappNotificationId(packageName, payload.id.trim().slice(0, MINIAPP_NOTIFICATION_ID_MAX)),
2876
+ source: packageName,
2877
+ timestamp: Date.now(),
2878
+ })
2879
+ this.sendResult(packageName, requestId, true)
2880
+ }
2881
+
2803
2882
  /**
2804
2883
  * Push bridged settings as they change: stored keys from the store, the
2805
2884
  * derived keys whenever their inputs move (phone activity, the display,
@@ -4149,7 +4228,7 @@ class LocalMiniappRuntime {
4149
4228
  }
4150
4229
 
4151
4230
  // ---------------------------------------------------------------------------
4152
- // User API keys (Settings > API Keys)
4231
+ // User API keys (Settings > Privacy > API Keys)
4153
4232
  // ---------------------------------------------------------------------------
4154
4233
 
4155
4234
  /**
@@ -4217,7 +4296,7 @@ class LocalMiniappRuntime {
4217
4296
  * The request names a TIER; this handler resolves it to a provider+model
4218
4297
  * (Settings > AI Models dropdowns, or auto from saved keys) and then picks
4219
4298
  * one of two transport paths, decided by whether the user saved a key for
4220
- * the resolved provider (Settings > API Keys) — the phone is where both
4299
+ * the resolved provider (Settings > Privacy > API Keys) — the phone is where both
4221
4300
  * settings live, so this is the only place the choice can be made:
4222
4301
  *
4223
4302
  * - User key saved → the phone calls the vendor DIRECTLY via
@@ -4490,7 +4569,7 @@ class LocalMiniappRuntime {
4490
4569
  this.sendResult(packageName, requestId, false, undefined, {
4491
4570
  code: MiniappErrorCode.QUOTA_EXCEEDED,
4492
4571
  message:
4493
- "AI credits are used up. Get more in Settings > Membership, or add your own API key in Settings > API Keys.",
4572
+ "AI credits are used up. Get more in Settings > Membership, or add your own API key in Settings > Privacy > API Keys.",
4494
4573
  })
4495
4574
  return
4496
4575
  }
@@ -4744,7 +4823,11 @@ class LocalMiniappRuntime {
4744
4823
  * session.system.webLogin — host in-app browser sheet. Same contract as
4745
4824
  * scanQr: the seam presents a Modal and never clears miniapp foreground.
4746
4825
  */
4747
- private async handleWebLogin(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
4826
+ private async handleWebLogin(
4827
+ packageName: string,
4828
+ payload: Record<string, unknown>,
4829
+ requestId?: string,
4830
+ ): Promise<void> {
4748
4831
  try {
4749
4832
  const result = await invokeWebLoginSeam(getUiSeams().webLogin, payload, packageName)
4750
4833
  this.sendResult(packageName, requestId, true, result)
@@ -5385,6 +5468,9 @@ class LocalMiniappRuntime {
5385
5468
  */
5386
5469
  private buildCloudAudioSubscriptions(cloudStreams: Set<string>): AudioSubscription[] {
5387
5470
  const subs: AudioSubscription[] = []
5471
+ // One auto recognizer serves every transcription listener. Explicit-language
5472
+ // listeners are filtered at delivery; translations still need their own jobs.
5473
+ const hasAutoTranscription = cloudStreams.has("transcription:auto")
5388
5474
  // Union of every connected app's TRANSCRIPTION_CONFIG hints. Hints only
5389
5475
  // apply to the auto-mode TRANSCRIPTION subscription (the wire schema has
5390
5476
  // no hints field on specific-language sources — the language IS the hint
@@ -5395,6 +5481,7 @@ class LocalMiniappRuntime {
5395
5481
  for (const stream of cloudStreams) {
5396
5482
  if (stream.startsWith("transcription:")) {
5397
5483
  const lang = stream.substring("transcription:".length)
5484
+ if (hasAutoTranscription && lang !== "auto") continue
5398
5485
  const source = langSource(lang)
5399
5486
  subs.push({
5400
5487
  kind: "transcription",
@@ -5527,6 +5614,12 @@ class LocalMiniappRuntime {
5527
5614
  }
5528
5615
 
5529
5616
  private handleTranscriptionData(d: TranscriptionData): void {
5617
+ const expectsAuto = [...(this.streamSubscribers.get("transcription:auto") ?? [])].some(
5618
+ (packageName) => this.appTranscriptionRoutesForSubscription(packageName, "transcription:auto").cloud,
5619
+ )
5620
+ // Subscription replacement is asynchronous. Ignore late results from the
5621
+ // old pipeline while switching between auto and language-specific jobs.
5622
+ if ((d.subscription.language.mode === "auto") !== expectsAuto) return
5530
5623
  const receivedAt = Date.now()
5531
5624
  if (TRANSCRIPT_TIMING_TELEMETRY) {
5532
5625
  console.log(
@@ -5565,6 +5658,8 @@ class LocalMiniappRuntime {
5565
5658
  __hostReceivedAt: receivedAt,
5566
5659
  },
5567
5660
  "cloud",
5661
+ undefined,
5662
+ d.subscription,
5568
5663
  )
5569
5664
  }
5570
5665
 
@@ -5642,8 +5737,7 @@ class LocalMiniappRuntime {
5642
5737
  /** Model profile + runtime overrides (the G2 CFW scene canvas) for the default wearable. */
5643
5738
  private currentCapabilities(): Capabilities {
5644
5739
  const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable) as
5645
- | DeviceTypes
5646
- | undefined
5740
+ DeviceTypes | undefined
5647
5741
  return getEffectiveCapabilities(defaultWearable || DeviceTypes.NONE)
5648
5742
  }
5649
5743
 
@@ -5661,7 +5755,7 @@ class LocalMiniappRuntime {
5661
5755
  // connected controllers add their input surfaces to `button`.
5662
5756
  useGlassesStore.subscribe(
5663
5757
  (s) =>
5664
- `${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}:${readyControllersKey(s.controllers)}`,
5758
+ `${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.sceneStereo === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}:${readyControllersKey(s.controllers)}`,
5665
5759
  broadcast,
5666
5760
  )
5667
5761
  // The debug knob flips hasNativeNavigation on custom firmware.
@@ -5720,28 +5814,33 @@ class LocalMiniappRuntime {
5720
5814
  }
5721
5815
  }
5722
5816
 
5723
- /** Host user preferences miniapps follow by default (Settings > Appearance). */
5817
+ /** Host user preferences miniapps follow by default: the metric toggle and the phone's own clock format. */
5724
5818
  private currentPreferences(): MiniappPreferences {
5725
5819
  const settings = useSettingsStore.getState()
5726
5820
  return {
5727
5821
  unitSystem: settings.getSetting(SETTINGS.metric_system.key) === true ? "metric" : "imperial",
5728
- twelveHourTime: settings.getSetting(SETTINGS.twelve_hour_time.key) === true,
5822
+ twelveHourTime: !uses24HourClock(),
5729
5823
  }
5730
5824
  }
5731
5825
 
5732
5826
  private ensurePreferencesWired(): void {
5733
5827
  if (this.preferencesWired) return
5734
5828
  this.preferencesWired = true
5735
- useSettingsStore.subscribe(
5736
- (s) =>
5737
- `${s.getSetting(SETTINGS.metric_system.key) === true}:${s.getSetting(SETTINGS.twelve_hour_time.key) === true}`,
5738
- () => {
5739
- const preferences = this.currentPreferences()
5740
- for (const packageName of this.handshookApps) {
5741
- this.sendToMiniapp(packageName, {type: MiniappResponseType.PREFERENCES_UPDATE, preferences})
5742
- }
5743
- },
5744
- )
5829
+ let sent = JSON.stringify(this.currentPreferences())
5830
+ const pushIfChanged = () => {
5831
+ const preferences = this.currentPreferences()
5832
+ const key = JSON.stringify(preferences)
5833
+ if (key === sent) return
5834
+ sent = key
5835
+ for (const packageName of this.handshookApps) {
5836
+ this.sendToMiniapp(packageName, {type: MiniappResponseType.PREFERENCES_UPDATE, preferences})
5837
+ }
5838
+ }
5839
+ useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.metric_system.key) === true, pushIfChanged)
5840
+ // The clock format is an OS setting, so it can only have changed while the app was in the background.
5841
+ AppState.addEventListener("change", (state) => {
5842
+ if (state === "active") pushIfChanged()
5843
+ })
5745
5844
  }
5746
5845
 
5747
5846
  private currentCloudStatus(): MiniappCloudStatus {
@@ -5790,6 +5889,8 @@ class LocalMiniappRuntime {
5790
5889
  transcriptionSource?: TranscriptionEventSource,
5791
5890
  /** Deliver to this subscriber alone (the designated dashboard app's gestures while it shows). */
5792
5891
  only?: string,
5892
+ /** Producing cloud/direct subscription; auto results also serve matching language listeners. */
5893
+ transcriptionSubscription?: TranscriptionData["subscription"],
5793
5894
  ): void {
5794
5895
  // Paused miniapps (glasses disconnected) hear no streams. The resume push
5795
5896
  // lands before the reconnect's own glasses_connection event, so nothing
@@ -5801,6 +5902,12 @@ class LocalMiniappRuntime {
5801
5902
  if (transcriptionSource === "local") markTranscriptSourceRecent("local")
5802
5903
  // Translate cloud event names to miniapp protocol stream types
5803
5904
  const normalizedStream = this.normalizeStreamType(streamType)
5905
+ const transcriptionStreams =
5906
+ transcriptionSource === "cloud" && transcriptionSubscription
5907
+ ? [...this.streamSubscribers.keys()].filter((stream) =>
5908
+ matchesTranscriptionSubscription(stream, normalizedStream, transcriptionSubscription),
5909
+ )
5910
+ : undefined
5804
5911
 
5805
5912
  // Passive memory capture happens HERE, before subscriber fan-out, so the
5806
5913
  // timeline reflects what was said around the user regardless of which
@@ -5809,10 +5916,10 @@ class LocalMiniappRuntime {
5809
5916
  // recorded while it was open.)
5810
5917
  this.captureTranscriptToMemory(normalizedStream, data)
5811
5918
 
5812
- // Collect all subscribers: exact match, plus wildcard matches for streams
5813
- // that carry a language tag. A miniapp subscribed to "transcription:auto"
5814
- // should receive any "transcription:<lang>" event (the detected language
5815
- // is conveyed in the event data, not the stream key).
5919
+ // Auto cloud/direct results serve auto listeners and listeners requesting
5920
+ // the detected language. Specific jobs serve only their own subscription.
5921
+ // Local/legacy events have no producing subscription and retain wildcard
5922
+ // matching so one on-device recognizer can serve auto and explicit listeners.
5816
5923
  //
5817
5924
  // Translation streams support cloud v3's wildcard patterns. Incoming
5818
5925
  // events are keyed `translation:<source>:<target>` and we match against
@@ -5824,11 +5931,13 @@ class LocalMiniappRuntime {
5824
5931
  // translation:auto — back-compat alias for *:*
5825
5932
  const matchedSubs = new Set<string>()
5826
5933
  let translationPatterns: string[] = []
5827
- const exact = this.streamSubscribers.get(normalizedStream)
5828
- if (exact) for (const p of exact) matchedSubs.add(p)
5934
+ for (const stream of transcriptionStreams ?? [normalizedStream]) {
5935
+ const exact = this.streamSubscribers.get(stream)
5936
+ if (exact) for (const p of exact) matchedSubs.add(p)
5937
+ }
5829
5938
 
5830
5939
  if (normalizedStream.startsWith("transcription:")) {
5831
- const autoSubs = this.streamSubscribers.get("transcription:auto")
5940
+ const autoSubs = transcriptionStreams ? undefined : this.streamSubscribers.get("transcription:auto")
5832
5941
  if (autoSubs) for (const p of autoSubs) matchedSubs.add(p)
5833
5942
  } else if (normalizedStream.startsWith("translation:")) {
5834
5943
  // translation:<source>:<target> — match each wildcard variant.
@@ -5907,8 +6016,18 @@ class LocalMiniappRuntime {
5907
6016
  // The wearer's touches reach a widget-only app through its host's widget events alone.
5908
6017
  if (normalizedStream === MiniappStreamType.TOUCH_EVENT && miniappWidgetMode.isWidgetOnly(packageName)) continue
5909
6018
  let transcriptionRoute: TranscriptionDeliveryRoute | undefined
6019
+ let outboundStream = normalizedStream
5910
6020
  if (normalizedStream.startsWith("transcription:") && transcriptionSource) {
5911
- const routes = this.appTranscriptionRoutesForEvent(packageName, normalizedStream)
6021
+ const cloudStreams = transcriptionStreams?.filter(
6022
+ (stream) => this.appTranscriptionRoutesForSubscription(packageName, stream).cloud,
6023
+ )
6024
+ const routes = cloudStreams
6025
+ ? {cloud: cloudStreams.length > 0, forceLocal: false}
6026
+ : this.appTranscriptionRoutesForEvent(packageName, normalizedStream)
6027
+ // Existing miniapp bundles dispatch explicit listeners by their BCP-47
6028
+ // subscription key, while providers detect bare codes (es vs es-ES).
6029
+ // Send one envelope per app, even when it also subscribes to auto.
6030
+ outboundStream = cloudStreams?.find((stream) => stream !== "transcription:auto") ?? normalizedStream
5912
6031
  transcriptionRoute =
5913
6032
  transcriptionDeliveryRoute(transcriptionSource, routes, this.transcriptionPrimaryUp()) ?? undefined
5914
6033
  if (!transcriptionRoute) continue
@@ -5937,7 +6056,7 @@ class LocalMiniappRuntime {
5937
6056
  }
5938
6057
  this.sendToMiniapp(packageName, {
5939
6058
  type: MiniappResponseType.EVENT,
5940
- streamType: normalizedStream,
6059
+ streamType: outboundStream,
5941
6060
  data: outboundData,
5942
6061
  ...(transcriptionRoute ? {transcriptionRoute} : {}),
5943
6062
  })
@@ -6233,6 +6352,7 @@ class LocalMiniappRuntime {
6233
6352
  }
6234
6353
 
6235
6354
  private miniappIcons = new MiniappIcons()
6355
+ private notificationSeq = 0
6236
6356
 
6237
6357
  private async handleMiniappsList(
6238
6358
  packageName: string,
@@ -6538,7 +6658,12 @@ class LocalMiniappRuntime {
6538
6658
  // and an unplace racing it cannot stop the provider early. A provider the
6539
6659
  // store already runs (a wearer's launch still spawning, say) stays a
6540
6660
  // normal run; the registry alone lags that launch.
6541
- this.widgetPlacements = placeWidget(this.widgetPlacements, {hostPackageName, providerPackageName: provider, widgetId, box})
6661
+ this.widgetPlacements = placeWidget(this.widgetPlacements, {
6662
+ hostPackageName,
6663
+ providerPackageName: provider,
6664
+ widgetId,
6665
+ box,
6666
+ })
6542
6667
  miniappWidgetMode.setPlaced(provider, true)
6543
6668
  if (!app.running && !miniappRunningRegistry.has(provider)) miniappWidgetMode.setWidgetOnly(provider, true)
6544
6669
  try {
@@ -6575,7 +6700,10 @@ class LocalMiniappRuntime {
6575
6700
  this.widgetPlacements = unplaceWidget(this.widgetPlacements, hostPackageName, provider, target.widgetId)
6576
6701
  this.sendResult(hostPackageName, requestId, true)
6577
6702
  if (!had) return
6578
- if (this.connectedApps.has(provider) && widgetHostsOf(this.widgetPlacements, provider, target.widgetId).length === 0) {
6703
+ if (
6704
+ this.connectedApps.has(provider) &&
6705
+ widgetHostsOf(this.widgetPlacements, provider, target.widgetId).length === 0
6706
+ ) {
6579
6707
  this.sendWidgetCall(provider, target.widgetId, {kind: "box", box: null})
6580
6708
  }
6581
6709
  await this.settleWidgetProvider(provider)
@@ -6706,9 +6834,7 @@ class LocalMiniappRuntime {
6706
6834
  if (!widget) return
6707
6835
  const view: WidgetView = payload.view === "expanded" ? "expanded" : "card"
6708
6836
  if (view === "expanded" && !widget.expandable) {
6709
- console.warn(
6710
- `${LOG_TAG}: dropping expanded frame from ${providerPackageName}/${widgetId}: it does not list EX`,
6711
- )
6837
+ console.warn(`${LOG_TAG}: dropping expanded frame from ${providerPackageName}/${widgetId}: it does not list EX`)
6712
6838
  return
6713
6839
  }
6714
6840
  const cap =
@@ -6733,8 +6859,7 @@ class LocalMiniappRuntime {
6733
6859
 
6734
6860
  /** A miniapp disconnected: as a host its placements go, as a provider its boxes are forgotten and its frames kept. */
6735
6861
  private onWidgetPartyGone(packageName: string): void {
6736
- const providersBefore = placedProviders(this.widgetPlacements)
6737
- this.widgetPlacements = dropWidgetHost(this.widgetPlacements, packageName)
6862
+ this.releaseWidgetHost(packageName)
6738
6863
  this.widgetPlacements = dropWidgetProvider(this.widgetPlacements, packageName)
6739
6864
  miniappWidgetMode.setPlaced(packageName, false)
6740
6865
  miniappWidgetMode.setWidgetOnly(packageName, false)
@@ -6749,8 +6874,13 @@ class LocalMiniappRuntime {
6749
6874
  })
6750
6875
  }
6751
6876
  }
6877
+ }
6878
+
6879
+ public releaseWidgetHost(hostPackageName: string): void {
6880
+ const providersBefore = placedProviders(this.widgetPlacements)
6881
+ this.widgetPlacements = dropWidgetHost(this.widgetPlacements, hostPackageName)
6752
6882
  for (const provider of providersBefore) {
6753
- if (provider !== packageName) void this.settleWidgetProvider(provider)
6883
+ if (provider !== hostPackageName) void this.settleWidgetProvider(provider)
6754
6884
  }
6755
6885
  }
6756
6886
 
@@ -28,11 +28,7 @@ import sttModelManager from "./STTModelManager"
28
28
  * `force_local` (a forced subscription would keep local STT on either way).
29
29
  */
30
30
  export type LocalSttFallbackReason =
31
- | "force_local"
32
- | "cloud_disconnected"
33
- | "cloud_disabled"
34
- | "stt_quota"
35
- | "provider_unreachable"
31
+ "force_local" | "cloud_disconnected" | "cloud_disabled" | "stt_quota" | "provider_unreachable"
36
32
 
37
33
  class LocalSttFallbackCoordinator {
38
34
  private static instance: LocalSttFallbackCoordinator
@@ -61,8 +61,7 @@ class NativeNavigationService {
61
61
  */
62
62
  public isSupported(): boolean {
63
63
  const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable) as
64
- | DeviceTypes
65
- | undefined
64
+ DeviceTypes | undefined
66
65
  // Effective, not model: custom firmware drops hasNativeNavigation so the
67
66
  // miniapp's scene HUD owns the display (see effectiveCapabilities).
68
67
  const capabilities = getEffectiveCapabilities(defaultWearable || DeviceTypes.NONE)
@@ -364,7 +364,7 @@ export class NavigationHandlers {
364
364
 
365
365
  async handleComputeRoute(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
366
366
  try {
367
- // With the user's own Mapbox key (Settings > API Keys), compute
367
+ // With the user's own Mapbox key (Settings > Privacy > API Keys), compute
368
368
  // on-device so routing works without the cloud and bills their account.
369
369
  // Without one, go through the v2 cloud maps proxy — org-token Mapbox
370
370
  // usage only ever happens cloud-side.
@@ -437,7 +437,7 @@ export class NavigationHandlers {
437
437
 
438
438
  /**
439
439
  * Type-ahead place search. Runs on-device against Mapbox Search Box when the
440
- * user saved their own key (Settings > API Keys) — that key exists precisely
440
+ * user saved their own key (Settings > Privacy > API Keys) — that key exists precisely
441
441
  * so search works without the cloud — and falls back to the v2 cloud maps
442
442
  * service otherwise. Resolves `{ok: true, suggestions}` — an empty query or no
443
443
  * matches is `{ok: true, suggestions: []}`, not an error.
@@ -211,7 +211,7 @@ class NavigationService {
211
211
  const mapboxRenderToken = await resolveMapboxRenderToken()
212
212
  if (!mapboxRenderToken) {
213
213
  const error =
214
- "No Mapbox token available — add your key in Settings > API Keys" +
214
+ "No Mapbox token available — add your key in Settings > Privacy > API Keys" +
215
215
  " (required while Glassly Cloud is disabled or unreachable)"
216
216
  console.warn(`${LOG_TAG}: start blocked — ${error}`)
217
217
  return {ok: false, error}
@@ -329,7 +329,7 @@ class NavigationService {
329
329
  * Compute one or more routes without starting a trip.
330
330
  *
331
331
  * Reached from the miniapp bridge (NavigationHandlers.handleComputeRoute)
332
- * when the user has saved their own Mapbox key in Settings > API Keys —
332
+ * when the user has saved their own Mapbox key in Settings > Privacy > API Keys —
333
333
  * that key exists so navigation works without the cloud. Without a user
334
334
  * key the bridge uses the v2 cloud maps service, which holds a provider
335
335
  * token server-side and caches results. Implemented by
@@ -379,7 +379,7 @@ class NavigationService {
379
379
  return !!resolveMapboxAccessToken()
380
380
  }
381
381
 
382
- /** True when the user saved their own Mapbox key in Settings > API Keys. */
382
+ /** True when the user saved their own Mapbox key in Settings > Privacy > API Keys. */
383
383
  public hasUserMapboxKey(): boolean {
384
384
  return !!userMapboxApiKey()
385
385
  }
@@ -501,7 +501,7 @@ export default navigationService
501
501
  // ---------------------------------------------------------------------------
502
502
 
503
503
  // Routes + Geocoding go DIRECT to Mapbox from the phone process ONLY when the
504
- // user saved their own key (Settings > API Keys) — "use my account". Without
504
+ // user saved their own key (Settings > Privacy > API Keys) — "use my account". Without
505
505
  // one, callers (NavigationHandlers) route these operations through the v2
506
506
  // cloud maps proxy instead; the binary ships no baked Mapbox key.
507
507
  const MAPBOX_DIRECTIONS_BASE = "https://api.mapbox.com/directions/v5/mapbox"
@@ -532,7 +532,7 @@ function mapboxToken(): string {
532
532
 
533
533
  /**
534
534
  * Phone-side helper: hit the Mapbox Directions API directly with the user's own
535
- * key (Settings > API Keys). Preferred over the v2 cloud maps service whenever
535
+ * key (Settings > Privacy > API Keys). Preferred over the v2 cloud maps service whenever
536
536
  * that key is set, so routing works without the cloud. Lives here
537
537
  * (not in Kotlin) so it's platform-independent across the JS bridge.
538
538
  * Returns the SDK-shaped result with primary + alternates.
@@ -583,7 +583,8 @@ async function computeRouteViaRoutesApi(payload: Record<string, unknown>): Promi
583
583
  }
584
584
 
585
585
  const token = mapboxToken()
586
- if (!token) return {ok: false, error: "computeRoute: no Mapbox key saved (add yours in Settings > API Keys)"}
586
+ if (!token)
587
+ return {ok: false, error: "computeRoute: no Mapbox key saved (add yours in Settings > Privacy > API Keys)"}
587
588
 
588
589
  // Mapbox path is a semicolon-joined `lng,lat` list: origin then each stop.
589
590
  const coords = [`${oLng},${oLat}`, ...stops.map((s) => `${s.lng},${s.lat}`)].join(";")
@@ -739,7 +740,7 @@ function mapboxManeuverToKind(type?: string, modifier?: string): string | undefi
739
740
 
740
741
  /**
741
742
  * Reverse-geocode a coordinate via the Mapbox Geocoding v6 API directly, using
742
- * the user's own key (Settings > API Keys). Preferred over the v2 cloud maps
743
+ * the user's own key (Settings > Privacy > API Keys). Preferred over the v2 cloud maps
743
744
  * service whenever that key is set, so pin labels work without the cloud.
744
745
  *
745
746
  * Returns the road/street name (e.g. "Octavia Blvd") plus the full formatted
@@ -767,7 +768,7 @@ async function reverseGeocodeRoadViaGeocodingApi(coord: {lat: number; lng: numbe
767
768
  if (!token)
768
769
  return {
769
770
  ok: false,
770
- error: "reverseGeocode: no Mapbox key saved (add yours in Settings > API Keys)",
771
+ error: "reverseGeocode: no Mapbox key saved (add yours in Settings > Privacy > API Keys)",
771
772
  }
772
773
  const params = new URLSearchParams({
773
774
  longitude: String(coord.lng),
@@ -813,7 +814,7 @@ async function reverseGeocodeRoadViaGeocodingApi(coord: {lat: number; lng: numbe
813
814
 
814
815
  /**
815
816
  * Type-ahead place search straight from the device via Mapbox Search Box,
816
- * using the user's own key (Settings > API Keys). This is the offline-from-
817
+ * using the user's own key (Settings > Privacy > API Keys). This is the offline-from-
817
818
  * the-cloud path: with a key saved, place search works with no runtime
818
819
  * involvement at all.
819
820
  *
@@ -840,7 +841,7 @@ async function placeAutocompleteViaSearchBox(
840
841
  if (!token) {
841
842
  return {
842
843
  ok: false,
843
- error: "placeAutocomplete: no Mapbox key saved (add yours in Settings > API Keys)",
844
+ error: "placeAutocomplete: no Mapbox key saved (add yours in Settings > Privacy > API Keys)",
844
845
  }
845
846
  }
846
847
  const params = new URLSearchParams({
@@ -904,7 +905,7 @@ async function placeDetailsViaSearchBox(
904
905
  if (!token) {
905
906
  return {
906
907
  ok: false,
907
- error: "placeDetails: no Mapbox key saved (add yours in Settings > API Keys)",
908
+ error: "placeDetails: no Mapbox key saved (add yours in Settings > Privacy > API Keys)",
908
909
  }
909
910
  }
910
911
  const params = new URLSearchParams({access_token: token, session_token: sessionToken})
@@ -40,9 +40,7 @@ import {useSettingsStore, SETTINGS} from "../stores/settings"
40
40
  import {DeviceTypes} from "../types"
41
41
 
42
42
  export type IdentitySnapshot =
43
- | {kind: "none"}
44
- | {kind: "pending"; model: string}
45
- | {kind: "paired"; model: string; name: string; address?: string}
43
+ {kind: "none"} | {kind: "pending"; model: string} | {kind: "paired"; model: string; name: string; address?: string}
46
44
 
47
45
  // Identity keys hold model/name strings; anything else (legacy null, a stray
48
46
  // non-string) reads as absent, matching native currentDefaultDevice()'s
@@ -14,9 +14,7 @@ import {
14
14
 
15
15
  export {PhoneCalendarError} from "./PhoneCalendarHelpers"
16
16
 
17
- type CalendarPermissionStatus = Awaited<
18
- ReturnType<typeof Calendar.getCalendarPermissionsAsync>
19
- >["status"]
17
+ type CalendarPermissionStatus = Awaited<ReturnType<typeof Calendar.getCalendarPermissionsAsync>>["status"]
20
18
 
21
19
  /**
22
20
  * expo-calendar reports anything below EventKit full access as not granted,
@@ -1,3 +1,4 @@
1
+ import {notificationAppPayload} from "./notificationAppPayload"
1
2
  /**
2
3
  * Phone-notifications sync — engine-owned, started by `engine.start()`. Keeps
3
4
  * three things in step with settings and the connected glasses, for any host:
@@ -78,7 +79,7 @@ export function getKnownNotificationApps(): NotificationApp[] {
78
79
  export function installedNotificationApps(): Promise<
79
80
  Array<{packageName: string; appName: string; icon: string | null}>
80
81
  > {
81
- if (Platform.OS === "android") return CrustModule.getInstalledApps()
82
+ if (Platform.OS === "android") return CrustModule.getInstalledApps().then(notificationAppPayload)
82
83
  if (Platform.OS !== "ios") return Promise.resolve([])
83
84
  return Promise.resolve(
84
85
  getKnownNotificationApps().map((app) => ({packageName: app.appId, appName: app.displayName, icon: null})),
@@ -339,8 +339,7 @@ export class PhoneStreamCoordinator {
339
339
  // block subsequent start/stop transitions on this coordinator.
340
340
  assertGlassesConnected()
341
341
  type JoinDecision =
342
- | {kind: "join"; entry: ManagedEntry; immediate: ManagedStartResult | null}
343
- | {kind: "fresh"; entry: ManagedEntry}
342
+ {kind: "join"; entry: ManagedEntry; immediate: ManagedStartResult | null} | {kind: "fresh"; entry: ManagedEntry}
344
343
 
345
344
  const decision = await this.runExclusive(async (): Promise<JoinDecision> => {
346
345
  if (this.current && this.current.kind === "unmanaged") {
@@ -2,7 +2,7 @@
2
2
  * ProviderKeyValidation
3
3
  *
4
4
  * Cheap validate-before-save probes for the AI/voice provider keys on the
5
- * Keys screen (Settings > API Keys), mirroring validateMapboxApiKey's shape:
5
+ * Keys screen (Settings > Privacy > API Keys), mirroring validateMapboxApiKey's shape:
6
6
  * each probe hits the provider's cheapest authenticated read endpoint (no
7
7
  * metered usage), maps auth failures to `invalid_key`, and maps everything
8
8
  * else to `network` so the caller can offer "save anyway".
@@ -151,12 +151,7 @@ class STTModelManager {
151
151
  fileName: "sherpa-onnx-nemo-streaming-fast-conformer-transducer-en-480ms-int8",
152
152
  size: 101 * 1024 * 1024,
153
153
  type: "transducer",
154
- requiredFiles: [
155
- "encoder.int8.onnx",
156
- "decoder.int8.onnx",
157
- "joiner.int8.onnx",
158
- "tokens.txt",
159
- ],
154
+ requiredFiles: ["encoder.int8.onnx", "decoder.int8.onnx", "joiner.int8.onnx", "tokens.txt"],
160
155
  languageCode: "en-US",
161
156
  },
162
157
  // NVIDIA Nemotron streaming ASR. These are model choices rather than plain