@glassly/engine 0.3.5 → 0.3.6-dev.846

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
@@ -32,7 +32,7 @@ import { useGlassesStore } from "../stores/glasses";
32
32
  import { SETTINGS, useSettingsStore } from "../stores/settings";
33
33
  import { markTranscriptSourceRecent, useTranscriptionDebugStore } from "../stores/transcriptionDebug";
34
34
  import { resolveMapboxRenderToken } from "./MapboxTokenService";
35
- import { createAnthropicProvider, createGeminiProvider, createOpenAiProvider, } from "@glassly/llm-providers";
35
+ import { createAnthropicProvider, createDeepSeekProvider, createGeminiProvider, createOpenAiProvider, } from "@glassly/llm-providers";
36
36
  import { normalizeAiTier, resolveAiTier } from "./AiTierResolution";
37
37
  import { decideLlmRoute, localLlmReason } from "./LlmRouting";
38
38
  import localLlmModelManager from "./LocalLlmModelManager";
@@ -82,6 +82,7 @@ import { invokeScanQrSeam } from "../runtime/scanQrSeam";
82
82
  import { invokeWebLoginSeam } from "../runtime/webLoginSeam";
83
83
  import { normalizeStreamAudioConfig, normalizeStreamVideoConfig } from "../runtime/streamConfig";
84
84
  import { visibleActionRows } from "./manifestActions";
85
+ import { actionFailure, appDeclaresAction, checkActionAccess, PendingActionCalls, } from "./actionInvoke";
85
86
  import { toLanguageHint } from "@glassly/cloud-protocol/languages";
86
87
  import { buildMiniappManifestSnapshot } from "../utils/miniappDiagnostics";
87
88
  import ttsModelManager from "./TTSModelManager";
@@ -95,6 +96,8 @@ import { listPhoneCalendarEvents, PhoneCalendarError } from "./PhoneCalendarServ
95
96
  import { LocalMiniappStorage } from "./LocalMiniappStorage";
96
97
  import { captureDecisionFor } from "./MemoryCapture";
97
98
  import { memoryStore } from "./memoryStoreInstance";
99
+ import { exportMemoryFile, importMemoryFile } from "./memoryBackupFile";
100
+ import { MemoryBackupError } from "./MemoryStore";
98
101
  import { speakerProfileStore } from "./speakerProfileStoreInstance";
99
102
  /** Streams that carry speech text and so have a cloud route and an on-device route. */
100
103
  const isSpeechTextStream = (stream) => stream.startsWith("transcription:") || stream.startsWith("translation:");
@@ -247,6 +250,7 @@ const MINIAPP_READABLE_API_KEYS = {
247
250
  anthropic: SETTINGS.anthropic_api_key.key,
248
251
  gemini: SETTINGS.gemini_api_key.key,
249
252
  openai: SETTINGS.openai_api_key.key,
253
+ deepseek: SETTINGS.deepseek_api_key.key,
250
254
  elevenlabs: SETTINGS.elevenlabs_api_key.key,
251
255
  mapbox: SETTINGS.mapbox_api_key.key,
252
256
  };
@@ -267,7 +271,9 @@ function directLlmProvider(name) {
267
271
  ? createGeminiProvider({ logger })
268
272
  : name === "openai"
269
273
  ? createOpenAiProvider({ logger })
270
- : createAnthropicProvider({ logger });
274
+ : name === "deepseek"
275
+ ? createDeepSeekProvider({ logger })
276
+ : createAnthropicProvider({ logger });
271
277
  directLlmProviders.set(name, built);
272
278
  return built;
273
279
  }
@@ -283,6 +289,7 @@ function savedLlmKeys() {
283
289
  anthropic: userLlmKey("anthropic").length > 0,
284
290
  gemini: userLlmKey("gemini").length > 0,
285
291
  openai: userLlmKey("openai").length > 0,
292
+ deepseek: userLlmKey("deepseek").length > 0,
286
293
  };
287
294
  }
288
295
  /** The stored Settings > AI Models dropdown value for a tier ("auto" default). */
@@ -438,6 +445,10 @@ class LocalMiniappRuntime {
438
445
  connectedApps = new Map();
439
446
  /** Claimed gestures (CLAIM_GESTURES): a stack per gesture, the most recent claimer holding it. */
440
447
  gestureClaims = emptyGestureClaims();
448
+ // The last main-view frame each app asked to render. Widget mode refuses
449
+ // frames and demotion clears them, and an app that has drawn once may not
450
+ // draw again on its own, so promotion replays this.
451
+ retainedRenders = new Map();
441
452
  /**
442
453
  * Packages that have completed their CONNECT handshake (sent
443
454
  * `miniapp_connect`, got `CONNECT_ACK`). Distinct from `connectedApps`,
@@ -490,7 +501,14 @@ class LocalMiniappRuntime {
490
501
  // secret never leaves the device.
491
502
  localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`;
492
503
  usedTokens = new Set();
493
- constructor() { }
504
+ constructor() {
505
+ miniappWidgetMode.subscribe((packageName, widgetOnly) => {
506
+ // Promotion happens inside the apps store's start(); the core-app slot
507
+ // settles with that store write, so replay once it has.
508
+ if (!widgetOnly)
509
+ queueMicrotask(() => this.replayRetainedRender(packageName));
510
+ });
511
+ }
494
512
  /** Snapshot of miniapps actively subscribed to hardware button events. */
495
513
  getButtonPressSubscribers() {
496
514
  return [...(this.streamSubscribers.get(MiniappStreamType.BUTTON_PRESS) ?? [])].sort();
@@ -874,7 +892,11 @@ class LocalMiniappRuntime {
874
892
  // window ends early once this app pushes its first display (see
875
893
  // LocalDisplayManager.request), or after ~1.5s. Fires once per spawn —
876
894
  // including a crash-respawn, since each spawn is a fresh "app is starting".
877
- localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName);
895
+ // A spawn for a dashboard widget is not the wearer starting the app: it draws
896
+ // nothing of its own, so it shows no boot text either.
897
+ if (!miniappWidgetMode.isWidgetOnly(packageName)) {
898
+ localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName);
899
+ }
878
900
  }
879
901
  /**
880
902
  * Resolve once `packageName` has completed its CONNECT handshake. If it has
@@ -996,6 +1018,7 @@ class LocalMiniappRuntime {
996
1018
  }
997
1019
  // Its gesture claims go back to whoever claimed them before.
998
1020
  this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName));
1021
+ this.retainedRenders.delete(packageName);
999
1022
  // Warm-up is a request-owned camera lease. Cancel it even if the app record was already
1000
1023
  // removed so a close racing the camera-open promise cannot leave the sensor running.
1001
1024
  // Keep camera teardown ordered: restoring FOV can restart the HAL, so the warm-up lease must
@@ -1300,6 +1323,8 @@ class LocalMiniappRuntime {
1300
1323
  case MiniappRequestType.MEMORY_QUERY:
1301
1324
  case MiniappRequestType.MEMORY_WINDOW:
1302
1325
  case MiniappRequestType.MEMORY_EXPORT_TEXT:
1326
+ case MiniappRequestType.MEMORY_EXPORT_FILE:
1327
+ case MiniappRequestType.MEMORY_IMPORT_FILE:
1303
1328
  case MiniappRequestType.MEMORY_SUMMARY_ADD:
1304
1329
  case MiniappRequestType.MEMORY_SUMMARY_LIST:
1305
1330
  case MiniappRequestType.MEMORY_PENDING_SUMMARY_HOUR:
@@ -1491,6 +1516,9 @@ class LocalMiniappRuntime {
1491
1516
  case MiniappRequestType.WIDGET_RESULT:
1492
1517
  this.handleWidgetResult(packageName, payload);
1493
1518
  break;
1519
+ case MiniappRequestType.WIDGET_EXPANDABLE:
1520
+ this.handleWidgetExpandable(packageName, payload, requestId);
1521
+ break;
1494
1522
  case MiniappRequestType.SYSTEM_EXIT:
1495
1523
  void this.handleSystemExit(packageName, requestId);
1496
1524
  break;
@@ -2124,11 +2152,6 @@ class LocalMiniappRuntime {
2124
2152
  this.sendResult(packageName, requestId, true, { status: "blocked", reason: "app stopped" });
2125
2153
  return;
2126
2154
  }
2127
- // A widget-only app draws through its widget alone.
2128
- if (miniappWidgetMode.isWidgetOnly(packageName)) {
2129
- this.sendResult(packageName, requestId, true, { status: "blocked", reason: "widget mode" });
2130
- return;
2131
- }
2132
2155
  if (!Array.isArray(payload.elements)) {
2133
2156
  this.sendResult(packageName, requestId, false, undefined, {
2134
2157
  code: MiniappErrorCode.INTERNAL,
@@ -2136,6 +2159,17 @@ class LocalMiniappRuntime {
2136
2159
  });
2137
2160
  return;
2138
2161
  }
2162
+ if (payload.view !== "dashboard") {
2163
+ if (payload.elements.length === 0)
2164
+ this.retainedRenders.delete(packageName);
2165
+ else
2166
+ this.retainedRenders.set(packageName, payload);
2167
+ }
2168
+ // A widget-only app draws through its widget alone; the frame waits for promotion.
2169
+ if (miniappWidgetMode.isWidgetOnly(packageName)) {
2170
+ this.sendResult(packageName, requestId, true, { status: "blocked", reason: "widget mode" });
2171
+ return;
2172
+ }
2139
2173
  // The dashboard view is the overlay app's alone; anyone else's frame
2140
2174
  // is dropped, reported rather than silently lost.
2141
2175
  if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
@@ -2160,6 +2194,13 @@ class LocalMiniappRuntime {
2160
2194
  });
2161
2195
  }
2162
2196
  }
2197
+ /** Put a promoted app's last main-view frame on the glasses; widget mode had refused it. */
2198
+ replayRetainedRender(packageName) {
2199
+ const payload = this.retainedRenders.get(packageName);
2200
+ if (!payload || !this.connectedApps.has(packageName) || miniappWidgetMode.isWidgetOnly(packageName))
2201
+ return;
2202
+ this.handleRender(packageName, payload);
2203
+ }
2163
2204
  /**
2164
2205
  * display.showView: the active dashboard takes or releases the overlay
2165
2206
  * slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
@@ -3433,6 +3474,14 @@ class LocalMiniappRuntime {
3433
3474
  this.sendResult(packageName, requestId, true, { text: await memoryStore.exportText(scope, num(payload.at)) });
3434
3475
  return;
3435
3476
  }
3477
+ case MiniappRequestType.MEMORY_EXPORT_FILE: {
3478
+ this.sendResult(packageName, requestId, true, await exportMemoryFile());
3479
+ return;
3480
+ }
3481
+ case MiniappRequestType.MEMORY_IMPORT_FILE: {
3482
+ this.sendResult(packageName, requestId, true, { imported: await importMemoryFile() });
3483
+ return;
3484
+ }
3436
3485
  case MiniappRequestType.MEMORY_SUMMARY_ADD: {
3437
3486
  const hourStart = num(payload.hourStart);
3438
3487
  if (hourStart == null || typeof payload.text !== "string") {
@@ -3469,6 +3518,13 @@ class LocalMiniappRuntime {
3469
3518
  }
3470
3519
  }
3471
3520
  catch (err) {
3521
+ if (err instanceof MemoryBackupError) {
3522
+ this.sendResult(packageName, requestId, false, undefined, {
3523
+ code: MiniappErrorCode.INVALID_ARGUMENT,
3524
+ message: err.message,
3525
+ });
3526
+ return;
3527
+ }
3472
3528
  console.error(`${LOG_TAG}: ${requestType} error:`, err);
3473
3529
  this.sendResult(packageName, requestId, false, undefined, {
3474
3530
  code: MiniappErrorCode.INTERNAL,
@@ -3985,7 +4041,7 @@ class LocalMiniappRuntime {
3985
4041
  if (base64) {
3986
4042
  // File share via base64 — write to temp file then share
3987
4043
  const tempFile = new File(Paths.cache, filename || "shared_file");
3988
- tempFile.write(base64, { encoding: "base64" });
4044
+ tempFile.writeSync(base64, { encoding: "base64" });
3989
4045
  await Share.open({
3990
4046
  url: tempFile.uri,
3991
4047
  type: mimeType || "application/octet-stream",
@@ -4055,7 +4111,7 @@ class LocalMiniappRuntime {
4055
4111
  let file;
4056
4112
  if (base64) {
4057
4113
  file = new File(Paths.cache, name);
4058
- file.write(base64, { encoding: "base64" });
4114
+ file.writeSync(base64, { encoding: "base64" });
4059
4115
  }
4060
4116
  else if (url) {
4061
4117
  file = await File.downloadFileAsync(url, new File(Paths.cache, name), { idempotent: true });
@@ -5224,7 +5280,7 @@ class LocalMiniappRuntime {
5224
5280
  /** Max serialized size of an action call's params / result (256 KB). */
5225
5281
  static ACTION_PAYLOAD_CAP = 256 * 1024;
5226
5282
  /** Outstanding action invocations, keyed by host-generated callId. */
5227
- actionCalls = new Map();
5283
+ actionCalls = new PendingActionCalls(BgTimer, (value) => this.actionPayloadTooLarge(value));
5228
5284
  actionCallSeq = 0;
5229
5285
  interopApps() {
5230
5286
  return useAppStatusStore.getState().apps;
@@ -5238,6 +5294,9 @@ class LocalMiniappRuntime {
5238
5294
  const app = this.interopApps().find((a) => a.packageName === packageName);
5239
5295
  if (!app)
5240
5296
  return false;
5297
+ // Running only for its widget: start() promotes it to a normal run, as opening it on the phone does.
5298
+ if (app.widgetOnly)
5299
+ return useAppStatusStore.getState().start(app, { skipNavigation: true });
5241
5300
  if (app.local) {
5242
5301
  await miniappLauncher.ensureConnected(packageName);
5243
5302
  return true;
@@ -5291,7 +5350,8 @@ class LocalMiniappRuntime {
5291
5350
  packageName: app.packageName,
5292
5351
  name: app.name,
5293
5352
  version: app.version ?? "",
5294
- running: app.running,
5353
+ // A provider running only for a placed widget is not a run the wearer started.
5354
+ running: app.running && !app.widgetOnly,
5295
5355
  hidden: app.hidden === true,
5296
5356
  system: isSystemMiniapp(app),
5297
5357
  compatibility,
@@ -5463,6 +5523,8 @@ class LocalMiniappRuntime {
5463
5523
  widgetCallSeq = 0;
5464
5524
  /** Elements per widget frame; a card is small, and the link is slow. */
5465
5525
  static WIDGET_FRAME_ELEMENT_CAP = 64;
5526
+ /** Elements per expanded widget frame: the full screen, on the same link. */
5527
+ static WIDGET_EXPANDED_FRAME_ELEMENT_CAP = 128;
5466
5528
  requireWidgetHost(packageName, requestId, op) {
5467
5529
  return this.requirePermission(packageName, requestId, "HOST_WIDGETS", `to ${op} widgets`);
5468
5530
  }
@@ -5505,6 +5567,7 @@ class LocalMiniappRuntime {
5505
5567
  name: widget.name,
5506
5568
  sizes: widget.sizes,
5507
5569
  defaultSize: widget.defaultSize,
5570
+ expandable: widget.expandOnTap,
5508
5571
  running: app.running,
5509
5572
  compatible: app.compatibility?.isCompatible !== false,
5510
5573
  });
@@ -5516,7 +5579,7 @@ class LocalMiniappRuntime {
5516
5579
  const box = raw;
5517
5580
  if (!box || typeof box !== "object")
5518
5581
  return null;
5519
- if (box.size !== "XS" && box.size !== "SM" && box.size !== "MD" && box.size !== "LG")
5582
+ if (box.size !== "XS" && box.size !== "XSM" && box.size !== "SM" && box.size !== "MD" && box.size !== "LG")
5520
5583
  return null;
5521
5584
  const width = Number(box.width);
5522
5585
  const height = Number(box.height);
@@ -5538,7 +5601,7 @@ class LocalMiniappRuntime {
5538
5601
  /**
5539
5602
  * session.widgets.place: record the host's box, wake the provider in widget
5540
5603
  * mode when it is not running, hand it the box, and answer with the last
5541
- * frame the provider pushed for the widget.
5604
+ * card and expanded frames the provider pushed for the widget.
5542
5605
  */
5543
5606
  async handleWidgetPlace(hostPackageName, payload, requestId) {
5544
5607
  if (!this.requireWidgetHost(hostPackageName, requestId, "place"))
@@ -5564,10 +5627,12 @@ class LocalMiniappRuntime {
5564
5627
  }
5565
5628
  const provider = app.packageName;
5566
5629
  // Record the placement before the wake so the spawn lands in widget mode
5567
- // and an unplace racing it cannot stop the provider early.
5630
+ // and an unplace racing it cannot stop the provider early. A provider the
5631
+ // store already runs (a wearer's launch still spawning, say) stays a
5632
+ // normal run; the registry alone lags that launch.
5568
5633
  this.widgetPlacements = placeWidget(this.widgetPlacements, { hostPackageName, providerPackageName: provider, widgetId, box });
5569
5634
  miniappWidgetMode.setPlaced(provider, true);
5570
- if (!miniappRunningRegistry.has(provider))
5635
+ if (!app.running && !miniappRunningRegistry.has(provider))
5571
5636
  miniappWidgetMode.setWidgetOnly(provider, true);
5572
5637
  try {
5573
5638
  await this.wakeInteropApp(provider);
@@ -5585,7 +5650,10 @@ class LocalMiniappRuntime {
5585
5650
  if (placementOf(this.widgetPlacements, hostPackageName, provider, widgetId) && this.connectedApps.has(provider)) {
5586
5651
  this.sendWidgetCall(provider, widgetId, { kind: "box", box });
5587
5652
  }
5588
- this.sendResult(hostPackageName, requestId, true, { frame: widgetFrameOf(this.widgetPlacements, provider, widgetId) });
5653
+ this.sendResult(hostPackageName, requestId, true, {
5654
+ frame: widgetFrameOf(this.widgetPlacements, provider, widgetId),
5655
+ expandedFrame: widgetFrameOf(this.widgetPlacements, provider, widgetId, "expanded"),
5656
+ });
5589
5657
  }
5590
5658
  async handleWidgetUnplace(hostPackageName, payload, requestId) {
5591
5659
  if (!this.requireWidgetHost(hostPackageName, requestId, "unplace"))
@@ -5702,22 +5770,54 @@ class LocalMiniappRuntime {
5702
5770
  this.sendResult(pending.hostPackageName, pending.hostRequestId, false, undefined, error);
5703
5771
  }
5704
5772
  }
5705
- /** Provider → host, one-shot: cache the frame and push it to every host holding the widget. */
5773
+ /** Provider: read, or set with `expandable`, whether a tap expands one of its own `EX` widgets. */
5774
+ handleWidgetExpandable(packageName, payload, requestId) {
5775
+ const widgetId = payload.widgetId;
5776
+ const app = this.interopApps().find((a) => a.packageName === packageName);
5777
+ const widget = app && widgetId ? this.declaredWidget(app, widgetId) : undefined;
5778
+ if (!widget?.expandable) {
5779
+ this.sendResult(packageName, requestId, false, undefined, {
5780
+ code: MiniappErrorCode.WIDGET_NOT_FOUND,
5781
+ message: `${packageName} has no widget "${widgetId ?? "(missing widgetId)"}" that lists EX`,
5782
+ });
5783
+ return;
5784
+ }
5785
+ const expandable = typeof payload.expandable === "boolean"
5786
+ ? useAppStatusStore.getState().setWidgetExpand(packageName, widget.id, payload.expandable)
5787
+ : widget.expandOnTap;
5788
+ this.sendResult(packageName, requestId, true, { expandable });
5789
+ }
5790
+ /** Provider → host, one-shot: cache the frame for its view and push it to every host holding the widget. */
5706
5791
  handleWidgetUpdate(providerPackageName, payload) {
5707
5792
  const widgetId = payload.widgetId;
5708
5793
  const elements = payload.elements;
5709
5794
  if (!widgetId || !Array.isArray(elements))
5710
5795
  return;
5711
5796
  const app = this.interopApps().find((a) => a.packageName === providerPackageName);
5712
- if (!app || !this.declaredWidget(app, widgetId))
5797
+ const widget = app ? this.declaredWidget(app, widgetId) : undefined;
5798
+ if (!widget)
5713
5799
  return;
5714
- if (elements.length > LocalMiniappRuntime.WIDGET_FRAME_ELEMENT_CAP || this.actionPayloadTooLarge(elements)) {
5715
- console.warn(`${LOG_TAG}: dropping oversized widget frame from ${providerPackageName}/${widgetId}`);
5800
+ const view = payload.view === "expanded" ? "expanded" : "card";
5801
+ if (view === "expanded" && !widget.expandable) {
5802
+ console.warn(`${LOG_TAG}: dropping expanded frame from ${providerPackageName}/${widgetId}: it does not list EX`);
5716
5803
  return;
5717
5804
  }
5718
- this.widgetPlacements = setWidgetFrame(this.widgetPlacements, providerPackageName, widgetId, elements);
5805
+ const cap = view === "expanded"
5806
+ ? LocalMiniappRuntime.WIDGET_EXPANDED_FRAME_ELEMENT_CAP
5807
+ : LocalMiniappRuntime.WIDGET_FRAME_ELEMENT_CAP;
5808
+ if (elements.length > cap || this.actionPayloadTooLarge(elements)) {
5809
+ console.warn(`${LOG_TAG}: dropping oversized ${view} widget frame from ${providerPackageName}/${widgetId}`);
5810
+ return;
5811
+ }
5812
+ this.widgetPlacements = setWidgetFrame(this.widgetPlacements, providerPackageName, widgetId, elements, view);
5719
5813
  for (const host of widgetHostsOf(this.widgetPlacements, providerPackageName, widgetId)) {
5720
- this.sendToMiniapp(host, { type: MiniappResponseType.WIDGET_FRAME, packageName: providerPackageName, widgetId, elements });
5814
+ this.sendToMiniapp(host, {
5815
+ type: MiniappResponseType.WIDGET_FRAME,
5816
+ packageName: providerPackageName,
5817
+ widgetId,
5818
+ elements,
5819
+ ...(view === "expanded" ? { view } : {}),
5820
+ });
5721
5821
  }
5722
5822
  }
5723
5823
  /** A miniapp disconnected: as a host its placements go, as a provider its boxes are forgotten and its frames kept. */
@@ -5754,104 +5854,82 @@ class LocalMiniappRuntime {
5754
5854
  return false;
5755
5855
  }
5756
5856
  }
5857
+ /** 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. */
5858
+ static clampActionTimeout(raw) {
5859
+ return Math.min(Math.max(Number(raw) || 30_000, 6_000), 120_000);
5860
+ }
5861
+ /** Caller identity the host presents for host-initiated actions (a system caller). */
5862
+ static HOST_ACTION_CALLER = "host";
5863
+ /** Installed, compatible, and declares the action — the same gates `invokeActionFromHost` applies up front. */
5864
+ hostActionAvailable(targetPackageName, actionId) {
5865
+ return appDeclaresAction(this.interopApps().find((a) => a.packageName === targetPackageName), actionId);
5866
+ }
5757
5867
  /**
5758
- * session.actions.invoke → wake the target headlessly, deliver an ACTION_CALL,
5759
- * and correlate its ACTION_RESULT back to the caller's pending request.
5868
+ * Host-initiated action call (e.g. the watch remote): same gates, headless
5869
+ * wake and ACTION_CALL/ACTION_RESULT correlation as a miniapp's
5870
+ * session.actions.invoke, with the host as a system caller.
5871
+ * Rejects with an Error whose `code` is a MiniappErrorCode.
5760
5872
  */
5873
+ async invokeActionFromHost(targetPackageName, actionId, params = {}, timeoutMs) {
5874
+ const outcome = await this.runActionInvoke(LocalMiniappRuntime.HOST_ACTION_CALLER, true, targetPackageName, actionId, params, LocalMiniappRuntime.clampActionTimeout(timeoutMs));
5875
+ if (outcome.ok)
5876
+ return outcome.data;
5877
+ throw Object.assign(new Error(outcome.error.message), { code: outcome.error.code });
5878
+ }
5879
+ /** session.actions.invoke → run the call, then relay the outcome to the caller's pending request. */
5761
5880
  async handleActionInvoke(callerPackageName, payload, requestId) {
5762
- const target = payload.targetPackageName;
5763
- const actionId = payload.actionId;
5764
- const params = payload.params ?? {};
5765
- // Floor the timeout at 6s so the host never rejects with ACTION_TIMEOUT before
5766
- // the target SDK's handler-registration window closes. A freshly-woken miniapp
5767
- // buffers an undelivered ACTION_CALL for HANDLER_WAIT_MS (5s in @glassly/miniapp's
5768
- // actions module) waiting for session.actions.handle; a shorter host timeout
5769
- // could fire while the target is still registering, then the action would run
5770
- // with no caller left to receive its result. 6s = that 5s buffer + 1s for the
5771
- // ACTION_RESULT/NO_ACTION_HANDLER reply to travel back. Keep these in sync.
5772
- const timeoutMs = Math.min(Math.max(Number(payload.timeoutMs) || 30_000, 6_000), 120_000);
5881
+ const outcome = await this.runActionInvoke(callerPackageName, this.isSystemPackage(callerPackageName), payload.targetPackageName, payload.actionId, payload.params ?? {}, LocalMiniappRuntime.clampActionTimeout(payload.timeoutMs));
5882
+ if (outcome.ok)
5883
+ this.sendResult(callerPackageName, requestId, true, outcome.data);
5884
+ else
5885
+ this.sendResult(callerPackageName, requestId, false, undefined, outcome.error);
5886
+ }
5887
+ /**
5888
+ * Gate, wake the target headlessly, deliver an ACTION_CALL, and resolve when
5889
+ * its ACTION_RESULT (or the timeout) arrives. Never rejects.
5890
+ */
5891
+ async runActionInvoke(callerPackageName, callerIsSystem, target, actionId, params, timeoutMs) {
5892
+ const fail = actionFailure;
5773
5893
  if (this.actionPayloadTooLarge(params)) {
5774
- this.sendResult(callerPackageName, requestId, false, undefined, {
5775
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
5776
- message: "action params exceeded the 256 KB cap",
5777
- });
5778
- return;
5894
+ return fail(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action params exceeded the 256 KB cap");
5779
5895
  }
5780
5896
  const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined;
5781
- if (!target || !app) {
5782
- this.sendResult(callerPackageName, requestId, false, undefined, {
5783
- code: MiniappErrorCode.APP_NOT_FOUND,
5784
- message: `Miniapp not found: ${target ?? "(missing targetPackageName)"}`,
5785
- });
5786
- return;
5787
- }
5788
- if (!actionId) {
5789
- this.sendResult(callerPackageName, requestId, false, undefined, {
5790
- code: MiniappErrorCode.ACTION_NOT_FOUND,
5791
- message: "invoke requires an actionId",
5792
- });
5793
- return;
5794
- }
5795
- if (app.compatibility && app.compatibility.isCompatible === false) {
5796
- this.sendResult(callerPackageName, requestId, false, undefined, {
5797
- code: MiniappErrorCode.APP_NOT_COMPATIBLE,
5798
- message: `${target} is not compatible with the connected glasses`,
5799
- });
5800
- islandNotifications.emit({
5801
- kind: "version_incompatible",
5802
- packageName: target,
5803
- reason: `${target} is not compatible with the connected glasses`,
5804
- metadata: { missingRequired: app.compatibility.missingRequired ?? [], via: "miniapps.invoke" },
5805
- timestamp: Date.now(),
5806
- });
5807
- return;
5808
- }
5809
- // Declared-action gate: the target must declare the action in its manifest.
5810
- const declared = (app.actions ?? []).find((a) => a.id === actionId);
5811
- if (!declared) {
5812
- this.sendResult(callerPackageName, requestId, false, undefined, {
5813
- code: MiniappErrorCode.ACTION_NOT_FOUND,
5814
- message: `${target} does not declare action "${actionId}"`,
5815
- });
5816
- return;
5817
- }
5818
- // Visibility gate: declared actions are public by default; ones marked
5819
- // `visibility: "system"` stay restricted to system/dev callers.
5820
- if (declared.visibility === "system" && !this.isSystemPackage(callerPackageName)) {
5821
- this.auditInterop({
5822
- caller: callerPackageName,
5823
- op: "invoke",
5824
- target,
5825
- actionId,
5826
- ok: false,
5827
- errorCode: MiniappErrorCode.NOT_PERMITTED,
5828
- });
5829
- this.sendResult(callerPackageName, requestId, false, undefined, {
5830
- code: MiniappErrorCode.NOT_PERMITTED,
5831
- message: `Action "${actionId}" on ${target} is restricted to system apps`,
5832
- });
5833
- return;
5897
+ const denied = checkActionAccess(app, target, actionId, callerIsSystem);
5898
+ if (denied) {
5899
+ if (denied.code === MiniappErrorCode.APP_NOT_COMPATIBLE && target && app) {
5900
+ islandNotifications.emit({
5901
+ kind: "version_incompatible",
5902
+ packageName: target,
5903
+ reason: denied.message,
5904
+ metadata: { missingRequired: app.compatibility?.missingRequired ?? [], via: "miniapps.invoke" },
5905
+ timestamp: Date.now(),
5906
+ });
5907
+ }
5908
+ else if (denied.code === MiniappErrorCode.NOT_PERMITTED) {
5909
+ this.auditInterop({
5910
+ caller: callerPackageName,
5911
+ op: "invoke",
5912
+ target,
5913
+ actionId,
5914
+ ok: false,
5915
+ errorCode: MiniappErrorCode.NOT_PERMITTED,
5916
+ });
5917
+ }
5918
+ return { ok: false, error: denied };
5834
5919
  }
5920
+ if (!target || !actionId)
5921
+ return fail(MiniappErrorCode.INTERNAL, "unreachable");
5835
5922
  // Headless wake + wait for CONNECT (idempotent / fast if already connected).
5836
5923
  try {
5837
5924
  await this.wakeInteropApp(target);
5838
5925
  }
5839
5926
  catch (e) {
5840
- this.sendResult(callerPackageName, requestId, false, undefined, {
5841
- code: MiniappErrorCode.WAKE_FAILED,
5842
- message: e?.message ?? `failed to wake ${target}`,
5843
- });
5844
- return;
5927
+ return fail(MiniappErrorCode.WAKE_FAILED, e?.message ?? `failed to wake ${target}`);
5845
5928
  }
5846
5929
  // The wake waited for CONNECT, but the target could have dropped in the gap
5847
5930
  // before delivery — fail fast rather than arming a timer for a call that
5848
- // sendToMiniapp would silently drop (the caller would otherwise wait the
5849
- // full invoke timeout for an ACTION_CALL that was never delivered).
5931
+ // sendToMiniapp would silently drop.
5850
5932
  if (!this.connectedApps.has(target)) {
5851
- this.sendResult(callerPackageName, requestId, false, undefined, {
5852
- code: MiniappErrorCode.WAKE_FAILED,
5853
- message: `${target} disconnected before the action could be delivered`,
5854
- });
5855
5933
  this.auditInterop({
5856
5934
  caller: callerPackageName,
5857
5935
  op: "invoke",
@@ -5860,18 +5938,11 @@ class LocalMiniappRuntime {
5860
5938
  ok: false,
5861
5939
  errorCode: MiniappErrorCode.WAKE_FAILED,
5862
5940
  });
5863
- return;
5941
+ return fail(MiniappErrorCode.WAKE_FAILED, `${target} disconnected before the action could be delivered`);
5864
5942
  }
5865
- // Deliver the call and arm the handler timeout; ACTION_RESULT resolves it.
5943
+ // Deliver the call and arm the handler timeout; ACTION_RESULT settles it.
5866
5944
  const callId = `act-${Date.now().toString(36)}-${this.actionCallSeq++}`;
5867
- const timer = BgTimer.setTimeout(() => {
5868
- this.actionCalls.delete(callId);
5869
- this.sendResult(callerPackageName, requestId, false, undefined, {
5870
- code: MiniappErrorCode.ACTION_TIMEOUT,
5871
- message: `action "${actionId}" timed out after ${timeoutMs}ms`,
5872
- });
5873
- }, timeoutMs);
5874
- this.actionCalls.set(callId, { callerPackageName, targetPackageName: target, callerRequestId: requestId, timer });
5945
+ const outcome = this.actionCalls.register(callId, target, actionId, timeoutMs);
5875
5946
  this.sendToMiniapp(target, {
5876
5947
  type: MiniappResponseType.ACTION_CALL,
5877
5948
  callId,
@@ -5880,40 +5951,11 @@ class LocalMiniappRuntime {
5880
5951
  callerPackageName,
5881
5952
  });
5882
5953
  this.auditInterop({ caller: callerPackageName, op: "invoke", target, actionId, ok: true });
5954
+ return outcome;
5883
5955
  }
5884
- /** Target → host: forward an ACTION_RESULT back to the caller's pending invoke. */
5956
+ /** Target → host: settle the pending invoke for this ACTION_RESULT. */
5885
5957
  handleActionResult(targetPackageName, payload) {
5886
- const callId = payload.callId;
5887
- if (!callId)
5888
- return;
5889
- const pending = this.actionCalls.get(callId);
5890
- if (!pending)
5891
- return;
5892
- // Only the invoked target may resolve this call. callIds are guessable
5893
- // (timestamp + seq), so a different connected miniapp must not be able to
5894
- // spoof a result/error for someone else's invoke.
5895
- if (pending.targetPackageName !== targetPackageName)
5896
- return;
5897
- this.actionCalls.delete(callId);
5898
- BgTimer.clearTimeout(pending.timer);
5899
- if (payload.ok === true) {
5900
- const result = payload.result;
5901
- if (this.actionPayloadTooLarge(result)) {
5902
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, {
5903
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
5904
- message: "action result exceeded the 256 KB cap",
5905
- });
5906
- return;
5907
- }
5908
- this.sendResult(pending.callerPackageName, pending.callerRequestId, true, result ?? null);
5909
- }
5910
- else {
5911
- const error = payload.error ?? {
5912
- code: MiniappErrorCode.INTERNAL,
5913
- message: "action handler error",
5914
- };
5915
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, error);
5916
- }
5958
+ this.actionCalls.handleResult(targetPackageName, payload);
5917
5959
  }
5918
5960
  sendResult(packageName, requestId, ok, data, error) {
5919
5961
  if (!requestId)