@glassly/engine 0.3.5 → 0.3.6-beta.851

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 (206) hide show
  1. package/build/engine.d.ts +11 -1
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +13 -1
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/ai.d.ts +3 -3
  6. package/build/facades/ai.d.ts.map +1 -1
  7. package/build/facades/ai.js +3 -3
  8. package/build/facades/ai.js.map +1 -1
  9. package/build/facades/glasses.d.ts +5 -1
  10. package/build/facades/glasses.d.ts.map +1 -1
  11. package/build/facades/glasses.js +39 -30
  12. package/build/facades/glasses.js.map +1 -1
  13. package/build/facades/memory.d.ts +10 -2
  14. package/build/facades/memory.d.ts.map +1 -1
  15. package/build/facades/memory.js +10 -0
  16. package/build/facades/memory.js.map +1 -1
  17. package/build/facades/miniapps.d.ts +2 -0
  18. package/build/facades/miniapps.d.ts.map +1 -1
  19. package/build/facades/miniapps.js +2 -0
  20. package/build/facades/miniapps.js.map +1 -1
  21. package/build/facades/pairing.d.ts.map +1 -1
  22. package/build/facades/pairing.js +3 -2
  23. package/build/facades/pairing.js.map +1 -1
  24. package/build/facades/providerKeys.d.ts +6 -2
  25. package/build/facades/providerKeys.d.ts.map +1 -1
  26. package/build/facades/providerKeys.js +6 -2
  27. package/build/facades/providerKeys.js.map +1 -1
  28. package/build/generated/releaseMetadata.js +4 -4
  29. package/build/generated/releaseMetadata.js.map +1 -1
  30. package/build/services/AiTierResolution.d.ts +3 -2
  31. package/build/services/AiTierResolution.d.ts.map +1 -1
  32. package/build/services/AiTierResolution.js +7 -2
  33. package/build/services/AiTierResolution.js.map +1 -1
  34. package/build/services/AppRegistry.d.ts +4 -1
  35. package/build/services/AppRegistry.d.ts.map +1 -1
  36. package/build/services/AppRegistry.js +16 -2
  37. package/build/services/AppRegistry.js.map +1 -1
  38. package/build/services/BlobStore.js +3 -3
  39. package/build/services/BlobStore.js.map +1 -1
  40. package/build/services/BluetoothRestoreLaunch.d.ts +8 -0
  41. package/build/services/BluetoothRestoreLaunch.d.ts.map +1 -0
  42. package/build/services/BluetoothRestoreLaunch.js +34 -0
  43. package/build/services/BluetoothRestoreLaunch.js.map +1 -0
  44. package/build/services/CloudClientService.d.ts +4 -4
  45. package/build/services/ForegroundLocationPermission.d.ts +1 -1
  46. package/build/services/ForegroundLocationPermission.d.ts.map +1 -1
  47. package/build/services/ForegroundLocationPermission.js.map +1 -1
  48. package/build/services/GestureDotsService.d.ts.map +1 -1
  49. package/build/services/GestureDotsService.js +2 -1
  50. package/build/services/GestureDotsService.js.map +1 -1
  51. package/build/services/GlassesTouchInput.d.ts +6 -4
  52. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  53. package/build/services/GlassesTouchInput.js +81 -41
  54. package/build/services/GlassesTouchInput.js.map +1 -1
  55. package/build/services/LocalDisplayManager.d.ts +12 -4
  56. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  57. package/build/services/LocalDisplayManager.js +21 -7
  58. package/build/services/LocalDisplayManager.js.map +1 -1
  59. package/build/services/LocalLlmModelManager.d.ts +3 -1
  60. package/build/services/LocalLlmModelManager.d.ts.map +1 -1
  61. package/build/services/LocalLlmModelManager.js +13 -5
  62. package/build/services/LocalLlmModelManager.js.map +1 -1
  63. package/build/services/LocalLlmPrompt.d.ts +10 -9
  64. package/build/services/LocalLlmPrompt.d.ts.map +1 -1
  65. package/build/services/LocalLlmPrompt.js +43 -32
  66. package/build/services/LocalLlmPrompt.js.map +1 -1
  67. package/build/services/LocalLlmProvider.d.ts +1 -0
  68. package/build/services/LocalLlmProvider.d.ts.map +1 -1
  69. package/build/services/LocalLlmProvider.js +62 -22
  70. package/build/services/LocalLlmProvider.js.map +1 -1
  71. package/build/services/LocalMiniappRuntime.d.ts +28 -6
  72. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  73. package/build/services/LocalMiniappRuntime.js +190 -148
  74. package/build/services/LocalMiniappRuntime.js.map +1 -1
  75. package/build/services/MemoryStore.d.ts +13 -1
  76. package/build/services/MemoryStore.d.ts.map +1 -1
  77. package/build/services/MemoryStore.js +98 -1
  78. package/build/services/MemoryStore.js.map +1 -1
  79. package/build/services/MiniappExitGesture.d.ts +6 -3
  80. package/build/services/MiniappExitGesture.d.ts.map +1 -1
  81. package/build/services/MiniappExitGesture.js +4 -4
  82. package/build/services/MiniappExitGesture.js.map +1 -1
  83. package/build/services/MiniappWidgetMode.d.ts +6 -4
  84. package/build/services/MiniappWidgetMode.d.ts.map +1 -1
  85. package/build/services/MiniappWidgetMode.js +7 -6
  86. package/build/services/MiniappWidgetMode.js.map +1 -1
  87. package/build/services/ProviderKeyValidation.d.ts +2 -0
  88. package/build/services/ProviderKeyValidation.d.ts.map +1 -1
  89. package/build/services/ProviderKeyValidation.js +4 -0
  90. package/build/services/ProviderKeyValidation.js.map +1 -1
  91. package/build/services/SceneRenderer.d.ts.map +1 -1
  92. package/build/services/SceneRenderer.js +3 -1
  93. package/build/services/SceneRenderer.js.map +1 -1
  94. package/build/services/ScreenshotFixture.d.ts +56 -0
  95. package/build/services/ScreenshotFixture.d.ts.map +1 -0
  96. package/build/services/ScreenshotFixture.js +72 -0
  97. package/build/services/ScreenshotFixture.js.map +1 -0
  98. package/build/services/actionInvoke.d.ts +37 -0
  99. package/build/services/actionInvoke.d.ts.map +1 -0
  100. package/build/services/actionInvoke.js +89 -0
  101. package/build/services/actionInvoke.js.map +1 -0
  102. package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
  103. package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
  104. package/build/services/appleWatch/AppleWatchLink.js +259 -0
  105. package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
  106. package/build/services/controllers.d.ts.map +1 -1
  107. package/build/services/controllers.js +16 -0
  108. package/build/services/controllers.js.map +1 -1
  109. package/build/services/manifestActions.d.ts +4 -2
  110. package/build/services/manifestActions.d.ts.map +1 -1
  111. package/build/services/manifestActions.js +9 -3
  112. package/build/services/manifestActions.js.map +1 -1
  113. package/build/services/memoryBackupFile.d.ts +9 -0
  114. package/build/services/memoryBackupFile.d.ts.map +1 -0
  115. package/build/services/memoryBackupFile.js +47 -0
  116. package/build/services/memoryBackupFile.js.map +1 -0
  117. package/build/services/settingsBridge.d.ts +4 -4
  118. package/build/services/settingsBridge.d.ts.map +1 -1
  119. package/build/services/settingsBridge.js +9 -4
  120. package/build/services/settingsBridge.js.map +1 -1
  121. package/build/services/widgetPlacements.d.ts +8 -7
  122. package/build/services/widgetPlacements.d.ts.map +1 -1
  123. package/build/services/widgetPlacements.js +7 -6
  124. package/build/services/widgetPlacements.js.map +1 -1
  125. package/build/stores/apps.d.ts +6 -0
  126. package/build/stores/apps.d.ts.map +1 -1
  127. package/build/stores/apps.js +29 -12
  128. package/build/stores/apps.js.map +1 -1
  129. package/build/stores/bluetoothSettingKeys.d.ts +2 -0
  130. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  131. package/build/stores/bluetoothSettingKeys.js +17 -0
  132. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  133. package/build/stores/settings.d.ts.map +1 -1
  134. package/build/stores/settings.js +42 -2
  135. package/build/stores/settings.js.map +1 -1
  136. package/build/types/applet.d.ts +5 -1
  137. package/build/types/applet.d.ts.map +1 -1
  138. package/build/types/applet.js.map +1 -1
  139. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  140. package/build/types/capabilities/rayneo-io.js +2 -0
  141. package/build/types/capabilities/rayneo-io.js.map +1 -1
  142. package/build/types/enums.d.ts +2 -1
  143. package/build/types/enums.d.ts.map +1 -1
  144. package/build/types/enums.js +1 -0
  145. package/build/types/enums.js.map +1 -1
  146. package/build/utils/cloudClient/RnUdpAdapter.d.ts.map +1 -1
  147. package/build/utils/cloudClient/RnUdpAdapter.js +13 -2
  148. package/build/utils/cloudClient/RnUdpAdapter.js.map +1 -1
  149. package/build/utils/display/scene/index.d.ts +1 -1
  150. package/build/utils/display/scene/index.d.ts.map +1 -1
  151. package/build/utils/display/scene/index.js.map +1 -1
  152. package/build/utils/display/scene/process.d.ts.map +1 -1
  153. package/build/utils/display/scene/process.js +20 -1
  154. package/build/utils/display/scene/process.js.map +1 -1
  155. package/build/utils/display/scene/types.d.ts +1 -1
  156. package/build/utils/display/scene/types.d.ts.map +1 -1
  157. package/build/utils/display/scene/types.js.map +1 -1
  158. package/build/utils/miniappGlobals.d.ts +6 -0
  159. package/build/utils/miniappGlobals.d.ts.map +1 -1
  160. package/build/utils/miniappGlobals.js +28 -4
  161. package/build/utils/miniappGlobals.js.map +1 -1
  162. package/package.json +9 -9
  163. package/src/engine.ts +13 -1
  164. package/src/facades/ai.ts +3 -3
  165. package/src/facades/glasses.ts +65 -42
  166. package/src/facades/memory.ts +11 -2
  167. package/src/facades/miniapps.ts +3 -0
  168. package/src/facades/pairing.ts +4 -2
  169. package/src/facades/providerKeys.ts +6 -1
  170. package/src/generated/releaseMetadata.ts +4 -4
  171. package/src/services/AiTierResolution.ts +9 -3
  172. package/src/services/AppRegistry.ts +18 -3
  173. package/src/services/BlobStore.ts +3 -3
  174. package/src/services/BluetoothRestoreLaunch.ts +37 -0
  175. package/src/services/ForegroundLocationPermission.ts +1 -1
  176. package/src/services/GestureDotsService.ts +2 -1
  177. package/src/services/GlassesTouchInput.ts +86 -38
  178. package/src/services/LocalDisplayManager.ts +22 -8
  179. package/src/services/LocalLlmModelManager.ts +16 -6
  180. package/src/services/LocalLlmPrompt.ts +56 -37
  181. package/src/services/LocalLlmProvider.ts +74 -24
  182. package/src/services/LocalMiniappRuntime.ts +228 -147
  183. package/src/services/MemoryStore.ts +111 -2
  184. package/src/services/MiniappExitGesture.ts +8 -5
  185. package/src/services/MiniappWidgetMode.ts +9 -7
  186. package/src/services/ProviderKeyValidation.ts +5 -0
  187. package/src/services/SceneRenderer.ts +3 -1
  188. package/src/services/ScreenshotFixture.ts +113 -0
  189. package/src/services/actionInvoke.ts +105 -0
  190. package/src/services/appleWatch/AppleWatchLink.ts +297 -0
  191. package/src/services/controllers.ts +17 -0
  192. package/src/services/manifestActions.ts +16 -4
  193. package/src/services/memoryBackupFile.ts +50 -0
  194. package/src/services/settingsBridge.ts +9 -4
  195. package/src/services/widgetPlacements.ts +23 -9
  196. package/src/stores/apps.ts +35 -11
  197. package/src/stores/bluetoothSettingKeys.ts +20 -0
  198. package/src/stores/settings.ts +44 -2
  199. package/src/types/applet.ts +5 -1
  200. package/src/types/capabilities/rayneo-io.ts +2 -0
  201. package/src/types/enums.ts +1 -0
  202. package/src/utils/cloudClient/RnUdpAdapter.ts +13 -1
  203. package/src/utils/display/scene/index.ts +1 -0
  204. package/src/utils/display/scene/process.ts +17 -1
  205. package/src/utils/display/scene/types.ts +1 -0
  206. package/src/utils/miniappGlobals.ts +30 -4
@@ -46,6 +46,7 @@ import {markTranscriptSourceRecent, useTranscriptionDebugStore} from "../stores/
46
46
  import {resolveMapboxRenderToken} from "./MapboxTokenService"
47
47
  import {
48
48
  createAnthropicProvider,
49
+ createDeepSeekProvider,
49
50
  createGeminiProvider,
50
51
  createOpenAiProvider,
51
52
  type LlmProviderImpl,
@@ -109,6 +110,7 @@ import {
109
110
  unplaceWidget,
110
111
  type WidgetBox,
111
112
  type WidgetPlacements,
113
+ type WidgetView,
112
114
  } from "./widgetPlacements"
113
115
  import {
114
116
  activeDashboard,
@@ -162,6 +164,13 @@ import {invokeScanQrSeam} from "../runtime/scanQrSeam"
162
164
  import {invokeWebLoginSeam} from "../runtime/webLoginSeam"
163
165
  import {normalizeStreamAudioConfig, normalizeStreamVideoConfig} from "../runtime/streamConfig"
164
166
  import {visibleActionRows} from "./manifestActions"
167
+ import {
168
+ actionFailure,
169
+ appDeclaresAction,
170
+ checkActionAccess,
171
+ PendingActionCalls,
172
+ type ActionOutcome,
173
+ } from "./actionInvoke"
165
174
  import {toLanguageHint} from "@glassly/cloud-protocol/languages"
166
175
  import {buildMiniappManifestSnapshot, type MiniappRuntimeDiagnosticSnapshot} from "../utils/miniappDiagnostics"
167
176
  import ttsModelManager from "./TTSModelManager"
@@ -176,6 +185,8 @@ import {listPhoneCalendarEvents, PhoneCalendarError} from "./PhoneCalendarServic
176
185
  import {LocalMiniappStorage} from "./LocalMiniappStorage"
177
186
  import {captureDecisionFor, type CaptureCandidate} from "./MemoryCapture"
178
187
  import {memoryStore} from "./memoryStoreInstance"
188
+ import {exportMemoryFile, importMemoryFile} from "./memoryBackupFile"
189
+ import {MemoryBackupError} from "./MemoryStore"
179
190
  import {speakerProfileStore} from "./speakerProfileStoreInstance"
180
191
 
181
192
  // =============================================================================
@@ -419,6 +430,7 @@ const MINIAPP_READABLE_API_KEYS: Record<string, string> = {
419
430
  anthropic: SETTINGS.anthropic_api_key.key,
420
431
  gemini: SETTINGS.gemini_api_key.key,
421
432
  openai: SETTINGS.openai_api_key.key,
433
+ deepseek: SETTINGS.deepseek_api_key.key,
422
434
  elevenlabs: SETTINGS.elevenlabs_api_key.key,
423
435
  mapbox: SETTINGS.mapbox_api_key.key,
424
436
  }
@@ -440,7 +452,9 @@ function directLlmProvider(name: AiTierProvider): LlmProviderImpl {
440
452
  ? createGeminiProvider({logger})
441
453
  : name === "openai"
442
454
  ? createOpenAiProvider({logger})
443
- : createAnthropicProvider({logger})
455
+ : name === "deepseek"
456
+ ? createDeepSeekProvider({logger})
457
+ : createAnthropicProvider({logger})
444
458
  directLlmProviders.set(name, built)
445
459
  return built
446
460
  }
@@ -460,6 +474,7 @@ function savedLlmKeys(): SavedLlmKeys {
460
474
  anthropic: userLlmKey("anthropic").length > 0,
461
475
  gemini: userLlmKey("gemini").length > 0,
462
476
  openai: userLlmKey("openai").length > 0,
477
+ deepseek: userLlmKey("deepseek").length > 0,
463
478
  }
464
479
  }
465
480
 
@@ -641,6 +656,10 @@ class LocalMiniappRuntime {
641
656
  private connectedApps: Map<string, ConnectedMiniapp> = new Map()
642
657
  /** Claimed gestures (CLAIM_GESTURES): a stack per gesture, the most recent claimer holding it. */
643
658
  private gestureClaims: GestureClaimState = emptyGestureClaims()
659
+ // The last main-view frame each app asked to render. Widget mode refuses
660
+ // frames and demotion clears them, and an app that has drawn once may not
661
+ // draw again on its own, so promotion replays this.
662
+ private retainedRenders = new Map<string, Record<string, unknown>>()
644
663
 
645
664
  /**
646
665
  * Packages that have completed their CONNECT handshake (sent
@@ -705,7 +724,13 @@ class LocalMiniappRuntime {
705
724
  private localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`
706
725
  private usedTokens = new Set<string>()
707
726
 
708
- private constructor() {}
727
+ private constructor() {
728
+ miniappWidgetMode.subscribe((packageName, widgetOnly) => {
729
+ // Promotion happens inside the apps store's start(); the core-app slot
730
+ // settles with that store write, so replay once it has.
731
+ if (!widgetOnly) queueMicrotask(() => this.replayRetainedRender(packageName))
732
+ })
733
+ }
709
734
 
710
735
  /** Snapshot of miniapps actively subscribed to hardware button events. */
711
736
  public getButtonPressSubscribers(): string[] {
@@ -1122,7 +1147,11 @@ class LocalMiniappRuntime {
1122
1147
  // window ends early once this app pushes its first display (see
1123
1148
  // LocalDisplayManager.request), or after ~1.5s. Fires once per spawn —
1124
1149
  // including a crash-respawn, since each spawn is a fresh "app is starting".
1125
- localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName)
1150
+ // A spawn for a dashboard widget is not the wearer starting the app: it draws
1151
+ // nothing of its own, so it shows no boot text either.
1152
+ if (!miniappWidgetMode.isWidgetOnly(packageName)) {
1153
+ localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName)
1154
+ }
1126
1155
  }
1127
1156
 
1128
1157
  /**
@@ -1244,6 +1273,7 @@ class LocalMiniappRuntime {
1244
1273
  }
1245
1274
  // Its gesture claims go back to whoever claimed them before.
1246
1275
  this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName))
1276
+ this.retainedRenders.delete(packageName)
1247
1277
 
1248
1278
  // Warm-up is a request-owned camera lease. Cancel it even if the app record was already
1249
1279
  // removed so a close racing the camera-open promise cannot leave the sensor running.
@@ -1569,6 +1599,8 @@ class LocalMiniappRuntime {
1569
1599
  case MiniappRequestType.MEMORY_QUERY:
1570
1600
  case MiniappRequestType.MEMORY_WINDOW:
1571
1601
  case MiniappRequestType.MEMORY_EXPORT_TEXT:
1602
+ case MiniappRequestType.MEMORY_EXPORT_FILE:
1603
+ case MiniappRequestType.MEMORY_IMPORT_FILE:
1572
1604
  case MiniappRequestType.MEMORY_SUMMARY_ADD:
1573
1605
  case MiniappRequestType.MEMORY_SUMMARY_LIST:
1574
1606
  case MiniappRequestType.MEMORY_PENDING_SUMMARY_HOUR:
@@ -1797,6 +1829,9 @@ class LocalMiniappRuntime {
1797
1829
  case MiniappRequestType.WIDGET_RESULT:
1798
1830
  this.handleWidgetResult(packageName, payload)
1799
1831
  break
1832
+ case MiniappRequestType.WIDGET_EXPANDABLE:
1833
+ this.handleWidgetExpandable(packageName, payload, requestId)
1834
+ break
1800
1835
  case MiniappRequestType.SYSTEM_EXIT:
1801
1836
  void this.handleSystemExit(packageName, requestId)
1802
1837
  break
@@ -2489,11 +2524,6 @@ class LocalMiniappRuntime {
2489
2524
  this.sendResult(packageName, requestId, true, {status: "blocked", reason: "app stopped"})
2490
2525
  return
2491
2526
  }
2492
- // A widget-only app draws through its widget alone.
2493
- if (miniappWidgetMode.isWidgetOnly(packageName)) {
2494
- this.sendResult(packageName, requestId, true, {status: "blocked", reason: "widget mode"})
2495
- return
2496
- }
2497
2527
  if (!Array.isArray(payload.elements)) {
2498
2528
  this.sendResult(packageName, requestId, false, undefined, {
2499
2529
  code: MiniappErrorCode.INTERNAL,
@@ -2501,6 +2531,15 @@ class LocalMiniappRuntime {
2501
2531
  })
2502
2532
  return
2503
2533
  }
2534
+ if (payload.view !== "dashboard") {
2535
+ if (payload.elements.length === 0) this.retainedRenders.delete(packageName)
2536
+ else this.retainedRenders.set(packageName, payload)
2537
+ }
2538
+ // A widget-only app draws through its widget alone; the frame waits for promotion.
2539
+ if (miniappWidgetMode.isWidgetOnly(packageName)) {
2540
+ this.sendResult(packageName, requestId, true, {status: "blocked", reason: "widget mode"})
2541
+ return
2542
+ }
2504
2543
  // The dashboard view is the overlay app's alone; anyone else's frame
2505
2544
  // is dropped, reported rather than silently lost.
2506
2545
  if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
@@ -2530,6 +2569,13 @@ class LocalMiniappRuntime {
2530
2569
  }
2531
2570
  }
2532
2571
 
2572
+ /** Put a promoted app's last main-view frame on the glasses; widget mode had refused it. */
2573
+ private replayRetainedRender(packageName: string): void {
2574
+ const payload = this.retainedRenders.get(packageName)
2575
+ if (!payload || !this.connectedApps.has(packageName) || miniappWidgetMode.isWidgetOnly(packageName)) return
2576
+ this.handleRender(packageName, payload)
2577
+ }
2578
+
2533
2579
  /**
2534
2580
  * display.showView: the active dashboard takes or releases the overlay
2535
2581
  * slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
@@ -3973,6 +4019,14 @@ class LocalMiniappRuntime {
3973
4019
  this.sendResult(packageName, requestId, true, {text: await memoryStore.exportText(scope, num(payload.at))})
3974
4020
  return
3975
4021
  }
4022
+ case MiniappRequestType.MEMORY_EXPORT_FILE: {
4023
+ this.sendResult(packageName, requestId, true, await exportMemoryFile())
4024
+ return
4025
+ }
4026
+ case MiniappRequestType.MEMORY_IMPORT_FILE: {
4027
+ this.sendResult(packageName, requestId, true, {imported: await importMemoryFile()})
4028
+ return
4029
+ }
3976
4030
  case MiniappRequestType.MEMORY_SUMMARY_ADD: {
3977
4031
  const hourStart = num(payload.hourStart)
3978
4032
  if (hourStart == null || typeof payload.text !== "string") {
@@ -4008,6 +4062,13 @@ class LocalMiniappRuntime {
4008
4062
  })
4009
4063
  }
4010
4064
  } catch (err) {
4065
+ if (err instanceof MemoryBackupError) {
4066
+ this.sendResult(packageName, requestId, false, undefined, {
4067
+ code: MiniappErrorCode.INVALID_ARGUMENT,
4068
+ message: err.message,
4069
+ })
4070
+ return
4071
+ }
4011
4072
  console.error(`${LOG_TAG}: ${requestType} error:`, err)
4012
4073
  this.sendResult(packageName, requestId, false, undefined, {
4013
4074
  code: MiniappErrorCode.INTERNAL,
@@ -4629,7 +4690,7 @@ class LocalMiniappRuntime {
4629
4690
  if (base64) {
4630
4691
  // File share via base64 — write to temp file then share
4631
4692
  const tempFile = new File(Paths.cache, filename || "shared_file")
4632
- tempFile.write(base64, {encoding: "base64"})
4693
+ tempFile.writeSync(base64, {encoding: "base64"})
4633
4694
  await Share.open({
4634
4695
  url: tempFile.uri,
4635
4696
  type: mimeType || "application/octet-stream",
@@ -4709,7 +4770,7 @@ class LocalMiniappRuntime {
4709
4770
  let file: File
4710
4771
  if (base64) {
4711
4772
  file = new File(Paths.cache, name)
4712
- file.write(base64, {encoding: "base64"})
4773
+ file.writeSync(base64, {encoding: "base64"})
4713
4774
  } else if (url) {
4714
4775
  file = await File.downloadFileAsync(url, new File(Paths.cache, name), {idempotent: true})
4715
4776
  } else {
@@ -5995,10 +6056,7 @@ class LocalMiniappRuntime {
5995
6056
  private static readonly ACTION_PAYLOAD_CAP = 256 * 1024
5996
6057
 
5997
6058
  /** Outstanding action invocations, keyed by host-generated callId. */
5998
- private actionCalls = new Map<
5999
- string,
6000
- {callerPackageName: string; targetPackageName: string; callerRequestId?: string; timer: number}
6001
- >()
6059
+ private actionCalls = new PendingActionCalls(BgTimer, (value) => this.actionPayloadTooLarge(value))
6002
6060
  private actionCallSeq = 0
6003
6061
 
6004
6062
  private interopApps(): ClientApp[] {
@@ -6013,6 +6071,8 @@ class LocalMiniappRuntime {
6013
6071
  private async startInteropApp(packageName: string): Promise<boolean> {
6014
6072
  const app = this.interopApps().find((a) => a.packageName === packageName)
6015
6073
  if (!app) return false
6074
+ // Running only for its widget: start() promotes it to a normal run, as opening it on the phone does.
6075
+ if (app.widgetOnly) return useAppStatusStore.getState().start(app, {skipNavigation: true})
6016
6076
  if (app.local) {
6017
6077
  await miniappLauncher.ensureConnected(packageName)
6018
6078
  return true
@@ -6074,7 +6134,8 @@ class LocalMiniappRuntime {
6074
6134
  packageName: app.packageName,
6075
6135
  name: app.name,
6076
6136
  version: app.version ?? "",
6077
- running: app.running,
6137
+ // A provider running only for a placed widget is not a run the wearer started.
6138
+ running: app.running && !app.widgetOnly,
6078
6139
  hidden: app.hidden === true,
6079
6140
  system: isSystemMiniapp(app),
6080
6141
  compatibility,
@@ -6268,6 +6329,8 @@ class LocalMiniappRuntime {
6268
6329
  private widgetCallSeq = 0
6269
6330
  /** Elements per widget frame; a card is small, and the link is slow. */
6270
6331
  private static readonly WIDGET_FRAME_ELEMENT_CAP = 64
6332
+ /** Elements per expanded widget frame: the full screen, on the same link. */
6333
+ private static readonly WIDGET_EXPANDED_FRAME_ELEMENT_CAP = 128
6271
6334
 
6272
6335
  private requireWidgetHost(packageName: string, requestId: string | undefined, op: string): boolean {
6273
6336
  return this.requirePermission(packageName, requestId, "HOST_WIDGETS", `to ${op} widgets`)
@@ -6316,6 +6379,7 @@ class LocalMiniappRuntime {
6316
6379
  name: widget.name,
6317
6380
  sizes: widget.sizes,
6318
6381
  defaultSize: widget.defaultSize,
6382
+ expandable: widget.expandOnTap,
6319
6383
  running: app.running,
6320
6384
  compatible: app.compatibility?.isCompatible !== false,
6321
6385
  })
@@ -6327,7 +6391,8 @@ class LocalMiniappRuntime {
6327
6391
  private parseWidgetBox(raw: unknown): WidgetBox | null {
6328
6392
  const box = raw as {size?: unknown; width?: unknown; height?: unknown} | null
6329
6393
  if (!box || typeof box !== "object") return null
6330
- if (box.size !== "XS" && box.size !== "SM" && box.size !== "MD" && box.size !== "LG") return null
6394
+ if (box.size !== "XS" && box.size !== "XSM" && box.size !== "SM" && box.size !== "MD" && box.size !== "LG")
6395
+ return null
6331
6396
  const width = Number(box.width)
6332
6397
  const height = Number(box.height)
6333
6398
  if (!(width > 0) || !(height > 0)) return null
@@ -6353,7 +6418,7 @@ class LocalMiniappRuntime {
6353
6418
  /**
6354
6419
  * session.widgets.place: record the host's box, wake the provider in widget
6355
6420
  * mode when it is not running, hand it the box, and answer with the last
6356
- * frame the provider pushed for the widget.
6421
+ * card and expanded frames the provider pushed for the widget.
6357
6422
  */
6358
6423
  private async handleWidgetPlace(
6359
6424
  hostPackageName: string,
@@ -6381,10 +6446,12 @@ class LocalMiniappRuntime {
6381
6446
  }
6382
6447
  const provider = app.packageName
6383
6448
  // Record the placement before the wake so the spawn lands in widget mode
6384
- // and an unplace racing it cannot stop the provider early.
6449
+ // and an unplace racing it cannot stop the provider early. A provider the
6450
+ // store already runs (a wearer's launch still spawning, say) stays a
6451
+ // normal run; the registry alone lags that launch.
6385
6452
  this.widgetPlacements = placeWidget(this.widgetPlacements, {hostPackageName, providerPackageName: provider, widgetId, box})
6386
6453
  miniappWidgetMode.setPlaced(provider, true)
6387
- if (!miniappRunningRegistry.has(provider)) miniappWidgetMode.setWidgetOnly(provider, true)
6454
+ if (!app.running && !miniappRunningRegistry.has(provider)) miniappWidgetMode.setWidgetOnly(provider, true)
6388
6455
  try {
6389
6456
  await this.wakeInteropApp(provider)
6390
6457
  } catch (e) {
@@ -6400,7 +6467,10 @@ class LocalMiniappRuntime {
6400
6467
  if (placementOf(this.widgetPlacements, hostPackageName, provider, widgetId) && this.connectedApps.has(provider)) {
6401
6468
  this.sendWidgetCall(provider, widgetId, {kind: "box", box})
6402
6469
  }
6403
- this.sendResult(hostPackageName, requestId, true, {frame: widgetFrameOf(this.widgetPlacements, provider, widgetId)})
6470
+ this.sendResult(hostPackageName, requestId, true, {
6471
+ frame: widgetFrameOf(this.widgetPlacements, provider, widgetId),
6472
+ expandedFrame: widgetFrameOf(this.widgetPlacements, provider, widgetId, "expanded"),
6473
+ })
6404
6474
  }
6405
6475
 
6406
6476
  private async handleWidgetUnplace(
@@ -6518,20 +6588,57 @@ class LocalMiniappRuntime {
6518
6588
  }
6519
6589
  }
6520
6590
 
6521
- /** Provider → host, one-shot: cache the frame and push it to every host holding the widget. */
6591
+ /** Provider: read, or set with `expandable`, whether a tap expands one of its own `EX` widgets. */
6592
+ private handleWidgetExpandable(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
6593
+ const widgetId = payload.widgetId as string | undefined
6594
+ const app = this.interopApps().find((a) => a.packageName === packageName)
6595
+ const widget = app && widgetId ? this.declaredWidget(app, widgetId) : undefined
6596
+ if (!widget?.expandable) {
6597
+ this.sendResult(packageName, requestId, false, undefined, {
6598
+ code: MiniappErrorCode.WIDGET_NOT_FOUND,
6599
+ message: `${packageName} has no widget "${widgetId ?? "(missing widgetId)"}" that lists EX`,
6600
+ })
6601
+ return
6602
+ }
6603
+ const expandable =
6604
+ typeof payload.expandable === "boolean"
6605
+ ? useAppStatusStore.getState().setWidgetExpand(packageName, widget.id, payload.expandable)
6606
+ : widget.expandOnTap
6607
+ this.sendResult(packageName, requestId, true, {expandable})
6608
+ }
6609
+
6610
+ /** Provider → host, one-shot: cache the frame for its view and push it to every host holding the widget. */
6522
6611
  private handleWidgetUpdate(providerPackageName: string, payload: Record<string, unknown>): void {
6523
6612
  const widgetId = payload.widgetId as string | undefined
6524
6613
  const elements = payload.elements
6525
6614
  if (!widgetId || !Array.isArray(elements)) return
6526
6615
  const app = this.interopApps().find((a) => a.packageName === providerPackageName)
6527
- if (!app || !this.declaredWidget(app, widgetId)) return
6528
- if (elements.length > LocalMiniappRuntime.WIDGET_FRAME_ELEMENT_CAP || this.actionPayloadTooLarge(elements)) {
6529
- console.warn(`${LOG_TAG}: dropping oversized widget frame from ${providerPackageName}/${widgetId}`)
6616
+ const widget = app ? this.declaredWidget(app, widgetId) : undefined
6617
+ if (!widget) return
6618
+ const view: WidgetView = payload.view === "expanded" ? "expanded" : "card"
6619
+ if (view === "expanded" && !widget.expandable) {
6620
+ console.warn(
6621
+ `${LOG_TAG}: dropping expanded frame from ${providerPackageName}/${widgetId}: it does not list EX`,
6622
+ )
6530
6623
  return
6531
6624
  }
6532
- this.widgetPlacements = setWidgetFrame(this.widgetPlacements, providerPackageName, widgetId, elements)
6625
+ const cap =
6626
+ view === "expanded"
6627
+ ? LocalMiniappRuntime.WIDGET_EXPANDED_FRAME_ELEMENT_CAP
6628
+ : LocalMiniappRuntime.WIDGET_FRAME_ELEMENT_CAP
6629
+ if (elements.length > cap || this.actionPayloadTooLarge(elements)) {
6630
+ console.warn(`${LOG_TAG}: dropping oversized ${view} widget frame from ${providerPackageName}/${widgetId}`)
6631
+ return
6632
+ }
6633
+ this.widgetPlacements = setWidgetFrame(this.widgetPlacements, providerPackageName, widgetId, elements, view)
6533
6634
  for (const host of widgetHostsOf(this.widgetPlacements, providerPackageName, widgetId)) {
6534
- this.sendToMiniapp(host, {type: MiniappResponseType.WIDGET_FRAME, packageName: providerPackageName, widgetId, elements})
6635
+ this.sendToMiniapp(host, {
6636
+ type: MiniappResponseType.WIDGET_FRAME,
6637
+ packageName: providerPackageName,
6638
+ widgetId,
6639
+ elements,
6640
+ ...(view === "expanded" ? {view} : {}),
6641
+ })
6535
6642
  }
6536
6643
  }
6537
6644
 
@@ -6568,111 +6675,118 @@ class LocalMiniappRuntime {
6568
6675
  }
6569
6676
  }
6570
6677
 
6678
+ /** Floored at 6s: a freshly-woken target buffers an undelivered ACTION_CALL for 5s (HANDLER_WAIT_MS in @glassly/miniapp's actions module) + 1s for the reply. Keep in sync. */
6679
+ private static clampActionTimeout(raw: unknown): number {
6680
+ return Math.min(Math.max(Number(raw) || 30_000, 6_000), 120_000)
6681
+ }
6682
+
6683
+ /** Caller identity the host presents for host-initiated actions (a system caller). */
6684
+ private static readonly HOST_ACTION_CALLER = "host"
6685
+
6686
+ /** Installed, compatible, and declares the action — the same gates `invokeActionFromHost` applies up front. */
6687
+ public hostActionAvailable(targetPackageName: string, actionId: string): boolean {
6688
+ return appDeclaresAction(
6689
+ this.interopApps().find((a) => a.packageName === targetPackageName),
6690
+ actionId,
6691
+ )
6692
+ }
6693
+
6571
6694
  /**
6572
- * session.actions.invoke → wake the target headlessly, deliver an ACTION_CALL,
6573
- * and correlate its ACTION_RESULT back to the caller's pending request.
6695
+ * Host-initiated action call (e.g. the watch remote): same gates, headless
6696
+ * wake and ACTION_CALL/ACTION_RESULT correlation as a miniapp's
6697
+ * session.actions.invoke, with the host as a system caller.
6698
+ * Rejects with an Error whose `code` is a MiniappErrorCode.
6574
6699
  */
6700
+ public async invokeActionFromHost(
6701
+ targetPackageName: string,
6702
+ actionId: string,
6703
+ params: Record<string, unknown> = {},
6704
+ timeoutMs?: number,
6705
+ ): Promise<unknown> {
6706
+ const outcome = await this.runActionInvoke(
6707
+ LocalMiniappRuntime.HOST_ACTION_CALLER,
6708
+ true,
6709
+ targetPackageName,
6710
+ actionId,
6711
+ params,
6712
+ LocalMiniappRuntime.clampActionTimeout(timeoutMs),
6713
+ )
6714
+ if (outcome.ok) return outcome.data
6715
+ throw Object.assign(new Error(outcome.error.message), {code: outcome.error.code})
6716
+ }
6717
+
6718
+ /** session.actions.invoke → run the call, then relay the outcome to the caller's pending request. */
6575
6719
  private async handleActionInvoke(
6576
6720
  callerPackageName: string,
6577
6721
  payload: Record<string, unknown>,
6578
6722
  requestId?: string,
6579
6723
  ): Promise<void> {
6580
- const target = payload.targetPackageName as string | undefined
6581
- const actionId = payload.actionId as string | undefined
6582
- const params = (payload.params as Record<string, unknown> | undefined) ?? {}
6583
- // Floor the timeout at 6s so the host never rejects with ACTION_TIMEOUT before
6584
- // the target SDK's handler-registration window closes. A freshly-woken miniapp
6585
- // buffers an undelivered ACTION_CALL for HANDLER_WAIT_MS (5s in @glassly/miniapp's
6586
- // actions module) waiting for session.actions.handle; a shorter host timeout
6587
- // could fire while the target is still registering, then the action would run
6588
- // with no caller left to receive its result. 6s = that 5s buffer + 1s for the
6589
- // ACTION_RESULT/NO_ACTION_HANDLER reply to travel back. Keep these in sync.
6590
- const timeoutMs = Math.min(Math.max(Number(payload.timeoutMs) || 30_000, 6_000), 120_000)
6724
+ const outcome = await this.runActionInvoke(
6725
+ callerPackageName,
6726
+ this.isSystemPackage(callerPackageName),
6727
+ payload.targetPackageName as string | undefined,
6728
+ payload.actionId as string | undefined,
6729
+ (payload.params as Record<string, unknown> | undefined) ?? {},
6730
+ LocalMiniappRuntime.clampActionTimeout(payload.timeoutMs),
6731
+ )
6732
+ if (outcome.ok) this.sendResult(callerPackageName, requestId, true, outcome.data)
6733
+ else this.sendResult(callerPackageName, requestId, false, undefined, outcome.error)
6734
+ }
6735
+
6736
+ /**
6737
+ * Gate, wake the target headlessly, deliver an ACTION_CALL, and resolve when
6738
+ * its ACTION_RESULT (or the timeout) arrives. Never rejects.
6739
+ */
6740
+ private async runActionInvoke(
6741
+ callerPackageName: string,
6742
+ callerIsSystem: boolean,
6743
+ target: string | undefined,
6744
+ actionId: string | undefined,
6745
+ params: Record<string, unknown>,
6746
+ timeoutMs: number,
6747
+ ): Promise<ActionOutcome> {
6748
+ const fail = actionFailure
6591
6749
 
6592
6750
  if (this.actionPayloadTooLarge(params)) {
6593
- this.sendResult(callerPackageName, requestId, false, undefined, {
6594
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
6595
- message: "action params exceeded the 256 KB cap",
6596
- })
6597
- return
6751
+ return fail(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action params exceeded the 256 KB cap")
6598
6752
  }
6599
6753
 
6600
6754
  const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined
6601
- if (!target || !app) {
6602
- this.sendResult(callerPackageName, requestId, false, undefined, {
6603
- code: MiniappErrorCode.APP_NOT_FOUND,
6604
- message: `Miniapp not found: ${target ?? "(missing targetPackageName)"}`,
6605
- })
6606
- return
6607
- }
6608
- if (!actionId) {
6609
- this.sendResult(callerPackageName, requestId, false, undefined, {
6610
- code: MiniappErrorCode.ACTION_NOT_FOUND,
6611
- message: "invoke requires an actionId",
6612
- })
6613
- return
6614
- }
6615
- if (app.compatibility && app.compatibility.isCompatible === false) {
6616
- this.sendResult(callerPackageName, requestId, false, undefined, {
6617
- code: MiniappErrorCode.APP_NOT_COMPATIBLE,
6618
- message: `${target} is not compatible with the connected glasses`,
6619
- })
6620
- islandNotifications.emit({
6621
- kind: "version_incompatible",
6622
- packageName: target,
6623
- reason: `${target} is not compatible with the connected glasses`,
6624
- metadata: {missingRequired: app.compatibility.missingRequired ?? [], via: "miniapps.invoke"},
6625
- timestamp: Date.now(),
6626
- })
6627
- return
6628
- }
6629
- // Declared-action gate: the target must declare the action in its manifest.
6630
- const declared = (app.actions ?? []).find((a) => a.id === actionId)
6631
- if (!declared) {
6632
- this.sendResult(callerPackageName, requestId, false, undefined, {
6633
- code: MiniappErrorCode.ACTION_NOT_FOUND,
6634
- message: `${target} does not declare action "${actionId}"`,
6635
- })
6636
- return
6637
- }
6638
- // Visibility gate: declared actions are public by default; ones marked
6639
- // `visibility: "system"` stay restricted to system/dev callers.
6640
- if (declared.visibility === "system" && !this.isSystemPackage(callerPackageName)) {
6641
- this.auditInterop({
6642
- caller: callerPackageName,
6643
- op: "invoke",
6644
- target,
6645
- actionId,
6646
- ok: false,
6647
- errorCode: MiniappErrorCode.NOT_PERMITTED,
6648
- })
6649
- this.sendResult(callerPackageName, requestId, false, undefined, {
6650
- code: MiniappErrorCode.NOT_PERMITTED,
6651
- message: `Action "${actionId}" on ${target} is restricted to system apps`,
6652
- })
6653
- return
6755
+ const denied = checkActionAccess(app, target, actionId, callerIsSystem)
6756
+ if (denied) {
6757
+ if (denied.code === MiniappErrorCode.APP_NOT_COMPATIBLE && target && app) {
6758
+ islandNotifications.emit({
6759
+ kind: "version_incompatible",
6760
+ packageName: target,
6761
+ reason: denied.message,
6762
+ metadata: {missingRequired: app.compatibility?.missingRequired ?? [], via: "miniapps.invoke"},
6763
+ timestamp: Date.now(),
6764
+ })
6765
+ } else if (denied.code === MiniappErrorCode.NOT_PERMITTED) {
6766
+ this.auditInterop({
6767
+ caller: callerPackageName,
6768
+ op: "invoke",
6769
+ target,
6770
+ actionId,
6771
+ ok: false,
6772
+ errorCode: MiniappErrorCode.NOT_PERMITTED,
6773
+ })
6774
+ }
6775
+ return {ok: false, error: denied}
6654
6776
  }
6777
+ if (!target || !actionId) return fail(MiniappErrorCode.INTERNAL, "unreachable")
6655
6778
 
6656
6779
  // Headless wake + wait for CONNECT (idempotent / fast if already connected).
6657
6780
  try {
6658
6781
  await this.wakeInteropApp(target)
6659
6782
  } catch (e) {
6660
- this.sendResult(callerPackageName, requestId, false, undefined, {
6661
- code: MiniappErrorCode.WAKE_FAILED,
6662
- message: (e as Error)?.message ?? `failed to wake ${target}`,
6663
- })
6664
- return
6783
+ return fail(MiniappErrorCode.WAKE_FAILED, (e as Error)?.message ?? `failed to wake ${target}`)
6665
6784
  }
6666
6785
 
6667
6786
  // The wake waited for CONNECT, but the target could have dropped in the gap
6668
6787
  // before delivery — fail fast rather than arming a timer for a call that
6669
- // sendToMiniapp would silently drop (the caller would otherwise wait the
6670
- // full invoke timeout for an ACTION_CALL that was never delivered).
6788
+ // sendToMiniapp would silently drop.
6671
6789
  if (!this.connectedApps.has(target)) {
6672
- this.sendResult(callerPackageName, requestId, false, undefined, {
6673
- code: MiniappErrorCode.WAKE_FAILED,
6674
- message: `${target} disconnected before the action could be delivered`,
6675
- })
6676
6790
  this.auditInterop({
6677
6791
  caller: callerPackageName,
6678
6792
  op: "invoke",
@@ -6681,19 +6795,12 @@ class LocalMiniappRuntime {
6681
6795
  ok: false,
6682
6796
  errorCode: MiniappErrorCode.WAKE_FAILED,
6683
6797
  })
6684
- return
6798
+ return fail(MiniappErrorCode.WAKE_FAILED, `${target} disconnected before the action could be delivered`)
6685
6799
  }
6686
6800
 
6687
- // Deliver the call and arm the handler timeout; ACTION_RESULT resolves it.
6801
+ // Deliver the call and arm the handler timeout; ACTION_RESULT settles it.
6688
6802
  const callId = `act-${Date.now().toString(36)}-${this.actionCallSeq++}`
6689
- const timer = BgTimer.setTimeout(() => {
6690
- this.actionCalls.delete(callId)
6691
- this.sendResult(callerPackageName, requestId, false, undefined, {
6692
- code: MiniappErrorCode.ACTION_TIMEOUT,
6693
- message: `action "${actionId}" timed out after ${timeoutMs}ms`,
6694
- })
6695
- }, timeoutMs)
6696
- this.actionCalls.set(callId, {callerPackageName, targetPackageName: target, callerRequestId: requestId, timer})
6803
+ const outcome = this.actionCalls.register(callId, target, actionId, timeoutMs)
6697
6804
  this.sendToMiniapp(target, {
6698
6805
  type: MiniappResponseType.ACTION_CALL,
6699
6806
  callId,
@@ -6702,38 +6809,12 @@ class LocalMiniappRuntime {
6702
6809
  callerPackageName,
6703
6810
  })
6704
6811
  this.auditInterop({caller: callerPackageName, op: "invoke", target, actionId, ok: true})
6812
+ return outcome
6705
6813
  }
6706
6814
 
6707
- /** Target → host: forward an ACTION_RESULT back to the caller's pending invoke. */
6815
+ /** Target → host: settle the pending invoke for this ACTION_RESULT. */
6708
6816
  private handleActionResult(targetPackageName: string, payload: Record<string, unknown>): void {
6709
- const callId = payload.callId as string | undefined
6710
- if (!callId) return
6711
- const pending = this.actionCalls.get(callId)
6712
- if (!pending) return
6713
- // Only the invoked target may resolve this call. callIds are guessable
6714
- // (timestamp + seq), so a different connected miniapp must not be able to
6715
- // spoof a result/error for someone else's invoke.
6716
- if (pending.targetPackageName !== targetPackageName) return
6717
- this.actionCalls.delete(callId)
6718
- BgTimer.clearTimeout(pending.timer)
6719
-
6720
- if (payload.ok === true) {
6721
- const result = payload.result
6722
- if (this.actionPayloadTooLarge(result)) {
6723
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, {
6724
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
6725
- message: "action result exceeded the 256 KB cap",
6726
- })
6727
- return
6728
- }
6729
- this.sendResult(pending.callerPackageName, pending.callerRequestId, true, result ?? null)
6730
- } else {
6731
- const error = (payload.error as ({code: string; message: string} & Record<string, unknown>) | undefined) ?? {
6732
- code: MiniappErrorCode.INTERNAL,
6733
- message: "action handler error",
6734
- }
6735
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, error)
6736
- }
6817
+ this.actionCalls.handleResult(targetPackageName, payload)
6737
6818
  }
6738
6819
 
6739
6820
  private sendResult(