@glassly/engine 0.3.1-dev.496 → 0.3.1-dev.523

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 (125) hide show
  1. package/build/engine.d.ts +9 -2
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +5 -0
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/dev.js +1 -1
  6. package/build/facades/dev.js.map +1 -1
  7. package/build/facades/miniapps.d.ts +5 -5
  8. package/build/facades/miniapps.d.ts.map +1 -1
  9. package/build/facades/miniapps.js +5 -6
  10. package/build/facades/miniapps.js.map +1 -1
  11. package/build/facades/speech.d.ts +10 -0
  12. package/build/facades/speech.d.ts.map +1 -1
  13. package/build/facades/speech.js +10 -0
  14. package/build/facades/speech.js.map +1 -1
  15. package/build/generated/releaseMetadata.js +3 -3
  16. package/build/generated/releaseMetadata.js.map +1 -1
  17. package/build/index.d.ts +1 -1
  18. package/build/index.d.ts.map +1 -1
  19. package/build/index.js +1 -1
  20. package/build/index.js.map +1 -1
  21. package/build/services/AppRegistry.d.ts +1 -7
  22. package/build/services/AppRegistry.d.ts.map +1 -1
  23. package/build/services/AppRegistry.js +15 -26
  24. package/build/services/AppRegistry.js.map +1 -1
  25. package/build/services/DevServerBridge.d.ts +1 -1
  26. package/build/services/DevServerBridge.js +2 -2
  27. package/build/services/DevServerBridge.js.map +1 -1
  28. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  29. package/build/services/DeviceEventRouter.js +2 -0
  30. package/build/services/DeviceEventRouter.js.map +1 -1
  31. package/build/services/JSRouter.js +1 -1
  32. package/build/services/JSRouter.js.map +1 -1
  33. package/build/services/LocalMiniappRuntime.d.ts +21 -6
  34. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  35. package/build/services/LocalMiniappRuntime.js +130 -48
  36. package/build/services/LocalMiniappRuntime.js.map +1 -1
  37. package/build/services/LocalTranslationService.d.ts +51 -0
  38. package/build/services/LocalTranslationService.d.ts.map +1 -0
  39. package/build/services/LocalTranslationService.js +141 -0
  40. package/build/services/LocalTranslationService.js.map +1 -0
  41. package/build/services/MiniappLauncher.d.ts +17 -23
  42. package/build/services/MiniappLauncher.d.ts.map +1 -1
  43. package/build/services/MiniappLauncher.js +30 -34
  44. package/build/services/MiniappLauncher.js.map +1 -1
  45. package/build/services/MiniappPauseCoordinator.d.ts +15 -0
  46. package/build/services/MiniappPauseCoordinator.d.ts.map +1 -0
  47. package/build/services/MiniappPauseCoordinator.js +93 -0
  48. package/build/services/MiniappPauseCoordinator.js.map +1 -0
  49. package/build/services/OfflineTranslationManager.d.ts +19 -0
  50. package/build/services/OfflineTranslationManager.d.ts.map +1 -0
  51. package/build/services/OfflineTranslationManager.js +35 -0
  52. package/build/services/OfflineTranslationManager.js.map +1 -0
  53. package/build/services/pages/OverlayPages.d.ts +5 -5
  54. package/build/services/pages/OverlayPages.d.ts.map +1 -1
  55. package/build/services/pages/OverlayPages.js +5 -5
  56. package/build/services/pages/OverlayPages.js.map +1 -1
  57. package/build/services/pages/overlayApps.d.ts +14 -11
  58. package/build/services/pages/overlayApps.d.ts.map +1 -1
  59. package/build/services/pages/overlayApps.js +21 -19
  60. package/build/services/pages/overlayApps.js.map +1 -1
  61. package/build/services/settingsBridge.d.ts +8 -8
  62. package/build/services/settingsBridge.d.ts.map +1 -1
  63. package/build/services/settingsBridge.js +12 -13
  64. package/build/services/settingsBridge.js.map +1 -1
  65. package/build/stores/apps.d.ts +0 -5
  66. package/build/stores/apps.d.ts.map +1 -1
  67. package/build/stores/apps.js +32 -52
  68. package/build/stores/apps.js.map +1 -1
  69. package/build/stores/settings.d.ts.map +1 -1
  70. package/build/stores/settings.js +0 -10
  71. package/build/stores/settings.js.map +1 -1
  72. package/build/types/applet.d.ts +3 -1
  73. package/build/types/applet.d.ts.map +1 -1
  74. package/build/types/applet.js.map +1 -1
  75. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  76. package/build/types/capabilities/even-realities-g1.js +1 -0
  77. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  78. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  79. package/build/types/capabilities/even-realities-g2.js +1 -0
  80. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  81. package/build/types/capabilities/mentra-display.d.ts.map +1 -1
  82. package/build/types/capabilities/mentra-display.js +1 -0
  83. package/build/types/capabilities/mentra-display.js.map +1 -1
  84. package/build/types/capabilities/nimo.d.ts.map +1 -1
  85. package/build/types/capabilities/nimo.js +1 -0
  86. package/build/types/capabilities/nimo.js.map +1 -1
  87. package/build/types/hardware.d.ts +9 -0
  88. package/build/types/hardware.d.ts.map +1 -1
  89. package/build/types/hardware.js +2 -0
  90. package/build/types/hardware.js.map +1 -1
  91. package/build/utils/miniappGlobals.js +1 -1
  92. package/build/utils/miniappGlobals.js.map +1 -1
  93. package/package.json +7 -7
  94. package/src/engine.ts +5 -0
  95. package/src/facades/dev.ts +1 -1
  96. package/src/facades/miniapps.ts +7 -7
  97. package/src/facades/speech.ts +12 -0
  98. package/src/generated/releaseMetadata.ts +3 -3
  99. package/src/index.ts +0 -2
  100. package/src/services/AppRegistry.ts +15 -27
  101. package/src/services/DevServerBridge.ts +2 -2
  102. package/src/services/DeviceEventRouter.ts +2 -0
  103. package/src/services/JSRouter.ts +1 -1
  104. package/src/services/LocalMiniappRuntime.ts +165 -66
  105. package/src/services/LocalTranslationService.ts +184 -0
  106. package/src/services/MiniappLauncher.ts +31 -34
  107. package/src/services/MiniappPauseCoordinator.ts +100 -0
  108. package/src/services/OfflineTranslationManager.ts +40 -0
  109. package/src/services/pages/OverlayPages.ts +5 -5
  110. package/src/services/pages/overlayApps.ts +19 -19
  111. package/src/services/settingsBridge.ts +12 -13
  112. package/src/stores/apps.ts +30 -58
  113. package/src/stores/settings.ts +0 -10
  114. package/src/types/applet.ts +3 -1
  115. package/src/types/capabilities/even-realities-g1.ts +1 -0
  116. package/src/types/capabilities/even-realities-g2.ts +1 -0
  117. package/src/types/capabilities/mentra-display.ts +1 -0
  118. package/src/types/capabilities/nimo.ts +1 -0
  119. package/src/types/hardware.ts +9 -0
  120. package/src/utils/miniappGlobals.ts +1 -1
  121. package/build/services/pages/activeDashboard.d.ts +0 -7
  122. package/build/services/pages/activeDashboard.d.ts.map +0 -1
  123. package/build/services/pages/activeDashboard.js +0 -21
  124. package/build/services/pages/activeDashboard.js.map +0 -1
  125. package/src/services/pages/activeDashboard.ts +0 -24
@@ -36,6 +36,9 @@ import { createAnthropicProvider, createGeminiProvider, createOpenAiProvider, }
36
36
  import { normalizeAiTier, resolveAiTier } from "./AiTierResolution";
37
37
  import { HttpError } from "@glassly/cloud-client";
38
38
  import localDisplayManager from "./LocalDisplayManager";
39
+ import sttModelManager from "./STTModelManager";
40
+ import localTranslationService from "./LocalTranslationService";
41
+ import { offlineTranslationSupported } from "./OfflineTranslationManager";
39
42
  import { dropGestureClaims, emptyGestureClaims, gestureClaimant, hasGestureClaim, setGestureClaims, } from "./gestureClaims";
40
43
  import overlayPages from "./pages/OverlayPages";
41
44
  import dashboardOpenTrace from "./debug/DashboardOpenTrace";
@@ -43,8 +46,7 @@ import { isAppForegrounded, isPhoneActive, onAppForegroundChange, onPhoneActivit
43
46
  import { installedNotificationApps } from "./PhoneNotificationsSync";
44
47
  import { DERIVED_SETTINGS_KEYS, SETTINGS_BRIDGE_KEYS, isDerivedSettingsKey, ringBatteryFrom, selectSettingsBridgeKeys, validateSettingsSet, } from "./settingsBridge";
45
48
  import { mayUsePermission } from "./miniappPermissions";
46
- import { activeDashboardAmong } from "./pages/activeDashboard";
47
- import { activeDashboard, addDashboardApp, displayViewsFor, isDashboardApp, isOverlayApp, onOverlayAppsChange, removeDashboardApp, setActiveDashboard, setOverlayViewer, } from "./pages/overlayApps";
49
+ import { activeDashboard, addDashboardApp, displayViewsFor, isDashboardApp, isOverlayApp, onOverlayAppsChange, removeDashboardApp, setOverlayViewer, } from "./pages/overlayApps";
48
50
  import { pagesEligible } from "./pages/pageEligibility";
49
51
  import nativeNavigationService from "./NativeNavigationService";
50
52
  import headingService from "./HeadingService";
@@ -52,6 +54,7 @@ import glassesSensorService from "./GlassesSensorService";
52
54
  import localSttFallbackCoordinator from "./LocalSttFallbackCoordinator";
53
55
  import speakerLabelResolver from "./SpeakerLabelResolver";
54
56
  import micStateCoordinator from "./MicStateCoordinator";
57
+ import { isMiniappsPaused, onMiniappsPauseChange } from "./MiniappPauseCoordinator";
55
58
  import sonioxDirectService from "./soniox/SonioxDirectService";
56
59
  import { BlobStore } from "./BlobStore";
57
60
  import { CloudAudioSubscriptionSync } from "./CloudAudioSubscriptionSync";
@@ -84,6 +87,8 @@ import { LocalMiniappStorage } from "./LocalMiniappStorage";
84
87
  import { captureDecisionFor } from "./MemoryCapture";
85
88
  import { memoryStore } from "./memoryStoreInstance";
86
89
  import { speakerProfileStore } from "./speakerProfileStoreInstance";
90
+ /** Streams that carry speech text and so have a cloud route and an on-device route. */
91
+ const isSpeechTextStream = (stream) => stream.startsWith("transcription:") || stream.startsWith("translation:");
87
92
  /**
88
93
  * Strictness ordering for `location_stream` rate values. Higher index =
89
94
  * higher accuracy and higher power cost. The aggregate tier applied to
@@ -428,6 +433,8 @@ class LocalMiniappRuntime {
428
433
  * Drives {@link waitForConnect}.
429
434
  */
430
435
  handshookApps = new Set();
436
+ /** Links that arrived before the target finished its handshake; sent right after CONNECT_ACK. */
437
+ pendingLinks = new Map();
431
438
  /** Pending {@link waitForConnect} resolvers, keyed by packageName. */
432
439
  connectWaiters = new Map();
433
440
  /** Ref-counted stream subscriptions: stream → set of packageNames. */
@@ -439,6 +446,7 @@ class LocalMiniappRuntime {
439
446
  cloudStatusWired = false;
440
447
  capabilitiesWired = false;
441
448
  preferencesWired = false;
449
+ pauseWired = false;
442
450
  settingsBridgeWired = false;
443
451
  /** Last derived read-only values pushed on the settings stream, for change detection. */
444
452
  derivedSettings = new Map();
@@ -845,6 +853,7 @@ class LocalMiniappRuntime {
845
853
  this.ensureCloudStatusWired();
846
854
  this.ensureCapabilitiesWired();
847
855
  this.ensurePreferencesWired();
856
+ this.ensurePauseWired();
848
857
  this.ensureSettingsBridgeWired();
849
858
  this.ensurePingLoop();
850
859
  // Show the system boot message ("Starting <name>…") on the glasses for the
@@ -961,6 +970,7 @@ class LocalMiniappRuntime {
961
970
  // (e.g. a wake that's mid-handshake when the app is torn down). Done before
962
971
  // the early-return so it runs even if the connectedApps entry is already gone.
963
972
  this.handshookApps.delete(packageName);
973
+ this.pendingLinks.delete(packageName);
964
974
  this.flushConnectWaiters(packageName, new Error(`${packageName} unregistered before connect`));
965
975
  // Drop this app's transcription hints; the cloud subscription rebuild
966
976
  // below (via replaceStreamSubscribers) re-derives the hint union without it.
@@ -1092,7 +1102,7 @@ class LocalMiniappRuntime {
1092
1102
  // Console-tap forwarding. The miniapp's console.log/warn/etc is wrapped
1093
1103
  // (via injected shim from miniappGlobals.ts) to post a `dev_log`
1094
1104
  // envelope. We fan out to two destinations:
1095
- // 1. DevServerBridge — forwards to the laptop's `glassly-miniapp dev`
1105
+ // 1. DevServerBridge — forwards to the laptop's `glassly dev`
1096
1106
  // terminal. No-op when there's no sidecar (installed miniapps).
1097
1107
  // 2. React Native console — surfaces the log in Metro / Xcode console
1098
1108
  // / adb logcat so installed-miniapp errors are still inspectable
@@ -1485,12 +1495,10 @@ class LocalMiniappRuntime {
1485
1495
  // Update lastPongAt so it doesn't time out right away
1486
1496
  existing.lastPongAt = Date.now();
1487
1497
  existing.unansweredPingRounds = 0;
1488
- // The dashboard that holds the role may take the overlay slot: its
1489
- // capabilities carry the dashboard view from the start.
1490
- if (existing.installedManifest?.type === "dashboard") {
1498
+ // The connected dashboard holds the role and may take the overlay slot:
1499
+ // its capabilities carry the dashboard view from the start.
1500
+ if (existing.installedManifest?.type === "dashboard")
1491
1501
  addDashboardApp(packageName);
1492
- this.syncActiveDashboard();
1493
- }
1494
1502
  const capabilities = this.capabilitiesFor(packageName);
1495
1503
  // Build the declared-permission record for the SDK's session.permissions
1496
1504
  // module. Lower-cased to match v3's PermissionType union (microphone,
@@ -1517,6 +1525,8 @@ class LocalMiniappRuntime {
1517
1525
  capabilities,
1518
1526
  preferences: this.currentPreferences(),
1519
1527
  visibility: this.currentVisibility(),
1528
+ paused: isMiniappsPaused(),
1529
+ ...(isMiniappsPaused() ? { pauseReason: "glasses_disconnected" } : {}),
1520
1530
  permissions: declaredPermissions,
1521
1531
  ...(existing.launchSource ? { launchSource: existing.launchSource } : {}),
1522
1532
  ...(initialAuth ? { auth: initialAuth } : {}),
@@ -1535,6 +1545,11 @@ class LocalMiniappRuntime {
1535
1545
  // Handshake complete — unblock any launcher.waitForConnect() callers.
1536
1546
  this.handshookApps.add(packageName);
1537
1547
  this.flushConnectWaiters(packageName);
1548
+ const pendingLink = this.pendingLinks.get(packageName);
1549
+ if (pendingLink) {
1550
+ this.pendingLinks.delete(packageName);
1551
+ this.sendToMiniapp(packageName, { type: MiniappResponseType.LINK, params: pendingLink });
1552
+ }
1538
1553
  }
1539
1554
  async requestMiniappAuth(packageName, opts) {
1540
1555
  const authPackageName = getDevAppSourcePackage(packageName) ?? packageName;
@@ -1791,15 +1806,15 @@ class LocalMiniappRuntime {
1791
1806
  }
1792
1807
  return "location_update";
1793
1808
  }
1794
- if (s.forceLocal === true && s.stream.startsWith("transcription:")) {
1809
+ if (s.forceLocal === true && isSpeechTextStream(s.stream)) {
1795
1810
  forceLocalTranscriptionStreams.add(s.stream);
1796
1811
  }
1797
- if (s.stream.startsWith("transcription:") && (s.forceLocal !== true || s.includeCloud === true)) {
1812
+ if (isSpeechTextStream(s.stream) && (s.forceLocal !== true || s.includeCloud === true)) {
1798
1813
  cloudTranscriptionStreams.add(s.stream);
1799
1814
  }
1800
1815
  return s.stream;
1801
1816
  }
1802
- if (s.startsWith("transcription:"))
1817
+ if (isSpeechTextStream(s))
1803
1818
  cloudTranscriptionStreams.add(s);
1804
1819
  return s;
1805
1820
  });
@@ -2197,8 +2212,6 @@ class LocalMiniappRuntime {
2197
2212
  return localDisplayManager.foregroundAppOnScreen();
2198
2213
  case "ring_battery":
2199
2214
  return ringBatteryFrom(useGlassesStore.getState());
2200
- case "active_dashboard":
2201
- return activeDashboard();
2202
2215
  }
2203
2216
  }
2204
2217
  handleSettingsGet(packageName, payload, requestId) {
@@ -4232,7 +4245,7 @@ class LocalMiniappRuntime {
4232
4245
  const previousSubscriptions = app.subscriptions;
4233
4246
  app.subscriptions = new Set(streams);
4234
4247
  app.forceLocalTranscriptionStreams.clear();
4235
- app.cloudTranscriptionStreams = new Set(streams.filter((stream) => stream.startsWith("transcription:")));
4248
+ app.cloudTranscriptionStreams = new Set(streams.filter(isSpeechTextStream));
4236
4249
  this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
4237
4250
  this.recomputeMicRequirements();
4238
4251
  this.updateCloudSubscriptions();
@@ -4250,6 +4263,14 @@ class LocalMiniappRuntime {
4250
4263
  loudnessGateEnabled: typeof configuredLoudnessGate === "boolean" ? configuredLoudnessGate : null,
4251
4264
  };
4252
4265
  }
4266
+ /**
4267
+ * Subscriptions that hold mic and transcription demand right now. A paused
4268
+ * app keeps its subscriptions registered (they come back on resume) but
4269
+ * demands nothing while the glasses are away.
4270
+ */
4271
+ activeStreamSubscribers() {
4272
+ return isMiniappsPaused() ? [] : this.streamSubscribers;
4273
+ }
4253
4274
  recomputeMicRequirements() {
4254
4275
  // Soniox-direct (user API key) consumes decoded PCM on the phone, so
4255
4276
  // transcription/translation subscriptions request PCM instead of LC3 —
@@ -4257,7 +4278,7 @@ class LocalMiniappRuntime {
4257
4278
  const sonioxDirect = sonioxDirectService.isEnabled();
4258
4279
  let anyPcm = false;
4259
4280
  let anyLc3 = false;
4260
- for (const [stream, subscribers] of this.streamSubscribers) {
4281
+ for (const [stream, subscribers] of this.activeStreamSubscribers()) {
4261
4282
  if (subscribers.size === 0)
4262
4283
  continue;
4263
4284
  if (stream === "audio_chunk")
@@ -4289,13 +4310,25 @@ class LocalMiniappRuntime {
4289
4310
  let transcriptionLang = null;
4290
4311
  let forceLocalTranscriptionLang = null;
4291
4312
  let hasForceLocalTranscription = false;
4292
- for (const [stream, subscribers] of this.streamSubscribers) {
4313
+ const localTranslations = [];
4314
+ for (const [stream, subscribers] of this.activeStreamSubscribers()) {
4293
4315
  if (subscribers.size === 0)
4294
4316
  continue;
4295
4317
  // Only transcription / translation need cloud delivery. Location and
4296
4318
  // notifications are sourced natively on the phone — no cloud hop.
4297
4319
  if (stream.startsWith("translation:")) {
4298
- cloudStreams.add(stream);
4320
+ const routes = summarizeTranscriptionRoutes(subscribers, (packageName) => this.appTranscriptionRoutesForSubscription(packageName, stream));
4321
+ if (routes.hasCloudSubscriber)
4322
+ cloudStreams.add(stream);
4323
+ const [, source, target, ...rest] = stream.split(":");
4324
+ if (target && target !== "*" && rest.length === 0) {
4325
+ localTranslations.push({
4326
+ source,
4327
+ target,
4328
+ cloud: routes.hasCloudSubscriber,
4329
+ forceLocal: routes.hasForceLocalSubscriber,
4330
+ });
4331
+ }
4299
4332
  }
4300
4333
  if (stream.startsWith("transcription:")) {
4301
4334
  const language = stream.substring("transcription:".length);
@@ -4311,7 +4344,14 @@ class LocalMiniappRuntime {
4311
4344
  cloudStreams.add(stream);
4312
4345
  }
4313
4346
  }
4314
- localSttFallbackCoordinator.onSubscriptionChange(transcriptionLang !== null, forceLocalTranscriptionLang ?? transcriptionLang, hasForceLocalTranscription);
4347
+ // On-device translation is fed by on-device transcripts, so a translation
4348
+ // subscription it can serve keeps local STT eligible exactly like a
4349
+ // transcription subscription does.
4350
+ const translatesLocally = offlineTranslationSupported() ? localTranslations : [];
4351
+ localTranslationService.setSubscriptions(translatesLocally);
4352
+ const hasForceLocalTranslation = translatesLocally.some((sub) => sub.forceLocal);
4353
+ const localSourceLang = translatesLocally.length > 0 ? sttModelManager.getBcp47ForLanguage() : null;
4354
+ localSttFallbackCoordinator.onSubscriptionChange(transcriptionLang !== null || localSourceLang !== null, forceLocalTranscriptionLang ?? transcriptionLang ?? localSourceLang, hasForceLocalTranscription || hasForceLocalTranslation);
4315
4355
  // Mirror the same set as typed AudioSubscription[]. When Soniox-direct is
4316
4356
  // enabled (user API key set), the DIRECT service serves the whole set and
4317
4357
  // the cloud gets an EMPTY one — pushed as a real PUT, not skipped, so
@@ -4437,6 +4477,11 @@ class LocalMiniappRuntime {
4437
4477
  markTranscriptSourceRecent("direct");
4438
4478
  this.handleTranslationData(d);
4439
4479
  });
4480
+ localTranslationService.attach({
4481
+ deliver: (stream, data) => this.forwardEvent(stream, data, "local"),
4482
+ primaryUp: () => this.transcriptionPrimaryUp(),
4483
+ onPairUnavailable: () => localDisplayManager.showSystemNotice("Offline translation language not downloaded.\nGet it in Settings > Offline models."),
4484
+ });
4440
4485
  // When the user sets/clears their Soniox key mid-session, re-partition
4441
4486
  // subscriptions and mic requirements without waiting for a subscription
4442
4487
  // change.
@@ -4533,7 +4578,7 @@ class LocalMiniappRuntime {
4533
4578
  didTranslate: true,
4534
4579
  provider: d.provider,
4535
4580
  confidence: d.confidence,
4536
- });
4581
+ }, "cloud");
4537
4582
  }
4538
4583
  /** Re-partition audio routing after a Soniox-direct enable/disable flip. */
4539
4584
  recomputeAudioRouting() {
@@ -4597,33 +4642,16 @@ class LocalMiniappRuntime {
4597
4642
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast);
4598
4643
  // The dashboard master switch flips the dashboard view.
4599
4644
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.custom_dashboard_enabled.key), broadcast);
4600
- // The wearer's choice and the installed dashboards decide who holds the role.
4601
- const syncRole = () => this.syncActiveDashboard();
4602
- useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.active_dashboard.key), syncRole);
4603
- useAppStatusStore.subscribe(syncRole);
4604
- syncRole();
4605
- }
4606
- /**
4607
- * Recompute who holds the dashboard role (ADR-0009). The previous holder
4608
- * gives up the slot first, so it hears the view change while it is still
4609
- * the overlay app; then every app sees its views again.
4610
- */
4611
- syncActiveDashboard() {
4612
- const candidates = [...useAppStatusStore.getState().apps];
4613
- // A dashboard can connect at launch before the apps store has listed it.
4614
- for (const [packageName, app] of this.connectedApps) {
4615
- if (candidates.some((candidate) => candidate.packageName === packageName))
4616
- continue;
4617
- candidates.push({ packageName, type: app.installedManifest?.type });
4618
- }
4619
- const next = activeDashboardAmong(candidates);
4620
- const previous = activeDashboard();
4621
- if (next === previous)
4622
- return;
4623
- if (previous !== null)
4624
- overlayPages.hide(previous, "replaced");
4625
- setActiveDashboard(next);
4626
- this.broadcastCapabilities();
4645
+ // The role follows the connected dashboard: when it passes, every app
4646
+ // sees its views again.
4647
+ let role = activeDashboard();
4648
+ onOverlayAppsChange(() => {
4649
+ const next = activeDashboard();
4650
+ if (next === role)
4651
+ return;
4652
+ role = next;
4653
+ broadcast();
4654
+ });
4627
4655
  }
4628
4656
  /** Capabilities as `packageName` sees them: the model profile plus the views this app may draw. */
4629
4657
  capabilitiesFor(packageName) {
@@ -4641,6 +4669,29 @@ class LocalMiniappRuntime {
4641
4669
  });
4642
4670
  }
4643
4671
  }
4672
+ /**
4673
+ * Pause/resume (MiniappPauseCoordinator) lands as one push per handshook
4674
+ * app, and the mic and cloud transcription demand follow it.
4675
+ */
4676
+ ensurePauseWired() {
4677
+ if (this.pauseWired)
4678
+ return;
4679
+ this.pauseWired = true;
4680
+ onMiniappsPauseChange((paused) => {
4681
+ this.broadcastPauseState(paused);
4682
+ this.recomputeMicRequirements();
4683
+ this.updateCloudSubscriptions();
4684
+ });
4685
+ }
4686
+ broadcastPauseState(paused) {
4687
+ for (const packageName of this.handshookApps) {
4688
+ this.sendToMiniapp(packageName, {
4689
+ type: MiniappResponseType.PAUSE_CHANGE,
4690
+ paused,
4691
+ ...(paused ? { reason: "glasses_disconnected" } : {}),
4692
+ });
4693
+ }
4694
+ }
4644
4695
  /** Host user preferences miniapps follow by default (Settings > Appearance). */
4645
4696
  currentPreferences() {
4646
4697
  const settings = useSettingsStore.getState();
@@ -4698,6 +4749,11 @@ class LocalMiniappRuntime {
4698
4749
  forwardEvent(streamType, data, transcriptionSource,
4699
4750
  /** Deliver to this subscriber alone (the designated dashboard app's gestures while it shows). */
4700
4751
  only) {
4752
+ // Paused miniapps (glasses disconnected) hear no streams. The resume push
4753
+ // lands before the reconnect's own glasses_connection event, so nothing
4754
+ // a resumed app needs is lost here.
4755
+ if (isMiniappsPaused())
4756
+ return;
4701
4757
  // Local sherpa results enter the runtime only through here (DeviceEventRouter's
4702
4758
  // local_transcription listener); cloud/direct are marked at their onTranscript
4703
4759
  // wiring, which never passes "local".
@@ -4725,6 +4781,7 @@ class LocalMiniappRuntime {
4725
4781
  // translation:<source>:<target> — exact (TranslationModule.fromTo)
4726
4782
  // translation:auto — back-compat alias for *:*
4727
4783
  const matchedSubs = new Set();
4784
+ let translationPatterns = [];
4728
4785
  const exact = this.streamSubscribers.get(normalizedStream);
4729
4786
  if (exact)
4730
4787
  for (const p of exact)
@@ -4741,13 +4798,13 @@ class LocalMiniappRuntime {
4741
4798
  // parts[0] = "translation", parts[1] = source, parts[2] = target
4742
4799
  if (parts.length === 3) {
4743
4800
  const [, source, target] = parts;
4744
- const patterns = [
4801
+ translationPatterns = [
4745
4802
  "translation:auto", // legacy alias for *:*
4746
4803
  "translation:*:*", // cloud v3 "all pairs"
4747
4804
  `translation:*:${target}`, // cloud v3 "to <target>"
4748
4805
  `translation:${source}:*`, // cloud v3 "from <source>"
4749
4806
  ];
4750
- for (const pat of patterns) {
4807
+ for (const pat of translationPatterns) {
4751
4808
  const subs = this.streamSubscribers.get(pat);
4752
4809
  if (subs)
4753
4810
  for (const p of subs)
@@ -4820,6 +4877,13 @@ class LocalMiniappRuntime {
4820
4877
  if (!transcriptionRoute)
4821
4878
  continue;
4822
4879
  }
4880
+ else if (normalizedStream.startsWith("translation:") && transcriptionSource) {
4881
+ const routes = this.appTranslationRoutesForEvent(packageName, [normalizedStream, ...translationPatterns]);
4882
+ transcriptionRoute =
4883
+ transcriptionDeliveryRoute(transcriptionSource, routes, this.transcriptionPrimaryUp()) ?? undefined;
4884
+ if (!transcriptionRoute)
4885
+ continue;
4886
+ }
4823
4887
  if (TRANSCRIPT_TIMING_TELEMETRY && normalizedStream.startsWith("transcription:")) {
4824
4888
  const transcript = data;
4825
4889
  const now = Date.now();
@@ -4905,6 +4969,13 @@ class LocalMiniappRuntime {
4905
4969
  forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true,
4906
4970
  };
4907
4971
  }
4972
+ appTranslationRoutesForEvent(packageName, patterns) {
4973
+ const app = this.connectedApps.get(packageName);
4974
+ return {
4975
+ cloud: patterns.some((pattern) => app?.cloudTranscriptionStreams.has(pattern) === true),
4976
+ forceLocal: patterns.some((pattern) => app?.forceLocalTranscriptionStreams.has(pattern) === true),
4977
+ };
4978
+ }
4908
4979
  appTranscriptionRoutesForEvent(packageName, stream) {
4909
4980
  const app = this.connectedApps.get(packageName);
4910
4981
  const autoStream = `${MiniappStreamType.TRANSCRIPTION}:auto`;
@@ -5415,6 +5486,17 @@ class LocalMiniappRuntime {
5415
5486
  ...(error ? { error } : {}),
5416
5487
  }, requestId);
5417
5488
  }
5489
+ /**
5490
+ * Hand an external link's query parameters to a miniapp. A miniapp that is
5491
+ * still spawning gets them once its handshake completes.
5492
+ */
5493
+ deliverLink(packageName, params) {
5494
+ if (!this.handshookApps.has(packageName)) {
5495
+ this.pendingLinks.set(packageName, params);
5496
+ return;
5497
+ }
5498
+ this.sendToMiniapp(packageName, { type: MiniappResponseType.LINK, params });
5499
+ }
5418
5500
  /**
5419
5501
  * Send a VISIBILITY_CHANGE push to a miniapp.
5420
5502
  */