@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
@@ -17,7 +17,14 @@
17
17
  * user-scoped keys so existing history survives the move.
18
18
  */
19
19
 
20
- import type {MemoryEntry, MemoryNoteSource, MemorySession, MemorySummary} from "@glassly/miniapp"
20
+ import type {
21
+ MemoryBackup,
22
+ MemoryEntry,
23
+ MemoryImportResult,
24
+ MemoryNoteSource,
25
+ MemorySession,
26
+ MemorySummary,
27
+ } from "@glassly/miniapp"
21
28
 
22
29
  import {searchMemoryEntries} from "../utils/memory/search"
23
30
 
@@ -91,6 +98,14 @@ export interface MemoryStats {
91
98
  oldestAt: number | null
92
99
  }
93
100
 
101
+ /** Thrown by importBackup for anything that isn't a backup this build can read. */
102
+ export class MemoryBackupError extends Error {
103
+ constructor(message: string) {
104
+ super(message)
105
+ this.name = "MemoryBackupError"
106
+ }
107
+ }
108
+
94
109
  export function hourStartOf(ms: number): number {
95
110
  return Math.floor(ms / HOUR_MS) * HOUR_MS
96
111
  }
@@ -359,6 +374,65 @@ export class MemoryStore {
359
374
  return Math.max(...hours)
360
375
  }
361
376
 
377
+ /** The whole timeline as a backup file body (Settings > Memory, session.memory.exportFile). */
378
+ async exportBackup(): Promise<MemoryBackup> {
379
+ await this.ensureLoaded()
380
+ return {
381
+ format: "glassly-memory",
382
+ version: 1,
383
+ exportedAt: Date.now(),
384
+ entries: [...this.entries],
385
+ summaries: [...this.summaries].sort((a, b) => a.hourStart - b.hourStart),
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Merge a backup into the timeline. Skips entries whose id is stored, notes
391
+ * whose text is already in the same hour, and hours that already have a
392
+ * summary. Throws MemoryBackupError when `data` isn't a memory backup.
393
+ */
394
+ async importBackup(data: unknown): Promise<MemoryImportResult> {
395
+ const backup = parseBackup(data)
396
+ await this.ensureLoaded()
397
+ const result: MemoryImportResult = {entries: 0, summaries: 0, skipped: 0}
398
+
399
+ const ids = new Set(this.entries.map((e) => e.id))
400
+ const noteKeys = new Set(this.entries.filter((e) => e.kind === "note").map(noteKey))
401
+ for (const entry of backup.entries) {
402
+ const key = entry.kind === "note" ? noteKey(entry) : null
403
+ if (ids.has(entry.id) || (key != null && noteKeys.has(key))) {
404
+ result.skipped++
405
+ continue
406
+ }
407
+ ids.add(entry.id)
408
+ if (key != null) noteKeys.add(key)
409
+ this.entries.push(entry)
410
+ result.entries++
411
+ }
412
+
413
+ const byHour = new Map(this.summaries.map((s) => [s.hourStart, s]))
414
+ for (const summary of backup.summaries) {
415
+ if (byHour.get(summary.hourStart)?.text.trim()) {
416
+ result.skipped++
417
+ continue
418
+ }
419
+ byHour.set(summary.hourStart, summary)
420
+ result.summaries++
421
+ }
422
+
423
+ if (result.entries > 0) {
424
+ this.entries.sort((a, b) => a.at - b.at)
425
+ this.prune()
426
+ await this.persistLog()
427
+ }
428
+ if (result.summaries > 0) {
429
+ this.summaries = [...byHour.values()].sort((a, b) => a.hourStart - b.hourStart).slice(-MEMORY_BOUNDS.maxSummaries)
430
+ await this.persistSummaries()
431
+ }
432
+ if (result.entries > 0 || result.summaries > 0) this.emitChanged()
433
+ return result
434
+ }
435
+
362
436
  /** Plain-text export of a scope, for the OS share sheet. */
363
437
  async exportText(scope: "session" | "day" | "all", at?: number): Promise<string> {
364
438
  const sessions = (await this.sessionList()).filter((s) => {
@@ -563,10 +637,45 @@ function parseArray(raw: unknown): unknown[] {
563
637
  function normalize(text: string): string {
564
638
  return text
565
639
  .toLowerCase()
566
- .replace(/[^a-z0-9]+/g, " ")
640
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
567
641
  .trim()
568
642
  }
569
643
 
644
+ function noteKey(entry: MemoryEntry): string {
645
+ return `${hourStartOf(entry.at)}:${normalize(entry.text)}`
646
+ }
647
+
648
+ function parseBackup(data: unknown): {entries: MemoryEntry[]; summaries: MemorySummary[]} {
649
+ const raw = data as Record<string, unknown> | null
650
+ if (!raw || typeof raw !== "object" || raw.format !== "glassly-memory" || !Array.isArray(raw.entries)) {
651
+ throw new MemoryBackupError("Not a Glassly memory backup")
652
+ }
653
+ if (typeof raw.version !== "number" || raw.version > 1) {
654
+ throw new MemoryBackupError(`Unsupported memory backup version ${String(raw.version)}`)
655
+ }
656
+ const entries: MemoryEntry[] = []
657
+ for (const v of raw.entries) {
658
+ if (!isEntry(v) || !v.id || !v.text.trim() || !Number.isFinite(v.at)) continue
659
+ entries.push({
660
+ id: v.id,
661
+ kind: v.kind,
662
+ text: v.text.trim(),
663
+ at: v.at,
664
+ speakerId: typeof v.speakerId === "string" ? v.speakerId : null,
665
+ ...(typeof v.resolvedSpeakerName === "string" && v.resolvedSpeakerName
666
+ ? {resolvedSpeakerName: v.resolvedSpeakerName}
667
+ : {}),
668
+ language: typeof v.language === "string" ? v.language : null,
669
+ ...(v.kind === "note" ? {source: v.source === "voice" || v.source === "auto" ? v.source : "ui"} : {}),
670
+ })
671
+ }
672
+ const summaries = (Array.isArray(raw.summaries) ? raw.summaries : [])
673
+ .filter(isSummary)
674
+ .filter((s) => s.text.trim() && Number.isFinite(s.hourStart))
675
+ .map((s) => ({hourStart: s.hourStart, text: s.text, createdAt: s.createdAt}))
676
+ return {entries, summaries}
677
+ }
678
+
570
679
  function isEntry(v: unknown): v is MemoryEntry {
571
680
  if (!v || typeof v !== "object") return false
572
681
  const e = v as Record<string, unknown>
@@ -4,6 +4,8 @@ interface ExitGestureApp {
4
4
  packageName: string
5
5
  name: string
6
6
  running: boolean
7
+ /** Running only for a placed widget: the wearer is not in it, so there is nothing to leave. */
8
+ widgetOnly?: boolean
7
9
  }
8
10
 
9
11
  interface ExitGestureState {
@@ -15,7 +17,8 @@ interface ExitGestureState {
15
17
 
16
18
  export interface ExitGestureDependencies {
17
19
  getState: () => ExitGestureState
18
- getVisiblePackageName: () => string | null
20
+ /** The miniapp a double tap leaves right now, or null when the tap is the system's. */
21
+ getTargetPackageName: () => string | null
19
22
  beginExitNotice: (packageName: string, text: string) => boolean
20
23
  finishExitNotice: (packageName: string) => void
21
24
  cancelExitNotice: (packageName: string) => void
@@ -29,16 +32,16 @@ export class MiniappExitGesture {
29
32
 
30
33
  public constructor(private readonly deps: ExitGestureDependencies) {}
31
34
 
32
- /** Returns true when the system gesture was consumed by a visible miniapp exit. */
33
- public tryExitVisibleMiniapp(): boolean {
35
+ /** Returns true when the system gesture was consumed by a miniapp exit. */
36
+ public tryExitMiniapp(): boolean {
34
37
  // Swallow repeat taps while the notice is already counting down.
35
38
  if (this.pending) return true
36
39
 
37
- const packageName = this.deps.getVisiblePackageName()
40
+ const packageName = this.deps.getTargetPackageName()
38
41
  if (!packageName) return false
39
42
 
40
43
  const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)
41
- if (!app?.running) return false
44
+ if (!app?.running || app.widgetOnly) return false
42
45
 
43
46
  const name = app.name.trim() || packageName
44
47
  if (!this.deps.beginExitNotice(packageName, `Exiting ${name}…`)) return false
@@ -2,25 +2,27 @@
2
2
  * Widget mode: a miniapp whose background context runs only because a host
3
3
  * (the dashboard) placed one of its widgets. It is running, but it has no
4
4
  * card in the app switcher, draws nothing of its own on the glasses and
5
- * hears no touch input; the Widgets pill lists it. Opening the app from the
6
- * home screen promotes it to a normal run; closing a placed app from the
7
- * tray demotes it back here instead of stopping it.
5
+ * hears no touch input; the Widgets pill lists it. Foregrounding the app on
6
+ * the phone promotes it to a normal run (the store's start/setForeground);
7
+ * closing a placed app from the tray demotes it back here instead of
8
+ * stopping it.
8
9
  *
9
10
  * The runtime is the writer (placements come and go through it); the apps
10
11
  * store projects `widgetOnly` from here beside `running`.
11
12
  */
12
13
 
13
- type Listener = () => void
14
+ /** Called with the app whose mode changed and whether it is now widget-only. */
15
+ type Listener = (packageName: string, widgetOnly: boolean) => void
14
16
 
15
17
  const widgetOnly = new Set<string>()
16
18
  /** Providers with at least one widget placed by some host. */
17
19
  const placed = new Set<string>()
18
20
  const listeners = new Set<Listener>()
19
21
 
20
- function notify(): void {
22
+ function notify(packageName: string, on: boolean): void {
21
23
  for (const fn of listeners) {
22
24
  try {
23
- fn()
25
+ fn(packageName, on)
24
26
  } catch (e) {
25
27
  console.warn("MiniappWidgetMode: listener threw", e)
26
28
  }
@@ -32,7 +34,7 @@ export const miniappWidgetMode = {
32
34
  setWidgetOnly(packageName: string, on: boolean): void {
33
35
  const changed = on ? !widgetOnly.has(packageName) : widgetOnly.delete(packageName)
34
36
  if (on) widgetOnly.add(packageName)
35
- if (changed) notify()
37
+ if (changed) notify(packageName, on)
36
38
  },
37
39
 
38
40
  isWidgetOnly(packageName: string): boolean {
@@ -51,6 +51,11 @@ export function validateOpenAiApiKey(apiKey: string): Promise<KeyValidationResul
51
51
  return probe("https://api.openai.com/v1/models", {Authorization: `Bearer ${apiKey.trim()}`}, [401, 403])
52
52
  }
53
53
 
54
+ /** GET /models with the candidate key; DeepSeek answers 401 for bad keys. */
55
+ export function validateDeepSeekApiKey(apiKey: string): Promise<KeyValidationResult> {
56
+ return probe("https://api.deepseek.com/models", {Authorization: `Bearer ${apiKey.trim()}`}, [401, 403])
57
+ }
58
+
54
59
  /** GET /v1/user with the candidate key; ElevenLabs answers 401 for bad keys. */
55
60
  export function validateElevenLabsApiKey(apiKey: string): Promise<KeyValidationResult> {
56
61
  return probe("https://api.elevenlabs.io/v1/user", {"xi-api-key": apiKey.trim()}, [401, 403])
@@ -66,7 +66,9 @@ const MAX_SETTLE_MS = 255 * 33
66
66
  function settleTimeOf(elements: FrameElement[]): number {
67
67
  let ms = 0
68
68
  for (const el of elements) {
69
- if (el.change !== "updated" && el.change !== "moved") continue
69
+ // Entering and leaving elements tween too, from or to their offset.
70
+ const edge = el.transition?.exit !== undefined || (el.change === "created" && el.transition?.enter !== undefined)
71
+ if (el.change !== "updated" && el.change !== "moved" && !edge) continue
70
72
  const t = el.transition?.durationMs ?? 0
71
73
  // Rotation has its own millisecond clock, beyond the geometry frame limit.
72
74
  const duration = Math.min(t, el.rotation !== undefined ? 65535 : MAX_SETTLE_MS)
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Screenshot fixture — dresses Phone Mode up as a paired Even Realities G2
3
+ * with a Meta Neural Band and an R1 ring, for the store-screenshot build only
4
+ * (EXPO_PUBLIC_SCREENSHOT_MODE=1, set by scripts/maestro-screenshots-*.mjs
5
+ * for its own Release build and never by a shipped one).
6
+ *
7
+ * The fixture is applied where hosts READ — engine.pairing.identity and
8
+ * engine.glasses.status/info/controllers.* — not where native writes: the
9
+ * Simulated driver keeps running underneath (so the mirror has frames),
10
+ * nothing is persisted, and native never learns of the fixture, so no
11
+ * reconnect, demotion or driver path can act on it. It is active only while
12
+ * the underlying identity is the simulated device: home enters Phone Mode as
13
+ * usual (identity `none` passes through untouched), the simulated pairing
14
+ * then flips the read-models to the fixture, and real glasses on a fixture
15
+ * build stay real.
16
+ */
17
+ import type {ControllerStatus, PairedController} from "@glassly/bluetooth-sdk"
18
+
19
+ import {ControllerTypes, DeviceTypes} from "../types/enums"
20
+ import type {IdentitySnapshot} from "./PairingIdentity"
21
+
22
+ export function isScreenshotMode(): boolean {
23
+ return process.env.EXPO_PUBLIC_SCREENSHOT_MODE === "1"
24
+ }
25
+
26
+ export const FIXTURE_GLASSES = {
27
+ model: DeviceTypes.G2,
28
+ name: "Even G2_7F3A",
29
+ battery: 87,
30
+ caseBattery: 100,
31
+ signal: -58,
32
+ } as const
33
+
34
+ interface FixtureController extends PairedController {
35
+ batteryLevel: number
36
+ }
37
+
38
+ // The R1's id is its ring id (from the BLE name); the band's is an opaque
39
+ // peripheral id. Both are stable fixtures so flows can target
40
+ // `controller-tile-<id>`.
41
+ export const FIXTURE_CONTROLLERS: readonly FixtureController[] = [
42
+ {id: "7C1A2B9E", model: ControllerTypes.NEURAL_BAND, name: "Meta Neural Band", batteryLevel: 73},
43
+ {id: "CEC5BA", model: ControllerTypes.R1, name: "CEC5BA", batteryLevel: 58},
44
+ ]
45
+
46
+ /** The fixture dresses Phone Mode only. */
47
+ export function fixtureActive(identity: IdentitySnapshot): boolean {
48
+ return isScreenshotMode() && identity.kind === "paired" && identity.model.includes(DeviceTypes.SIMULATED)
49
+ }
50
+
51
+ export function fixtureIdentity(identity: IdentitySnapshot): IdentitySnapshot {
52
+ if (!fixtureActive(identity)) return identity
53
+ return {kind: "paired", model: FIXTURE_GLASSES.model, name: FIXTURE_GLASSES.name}
54
+ }
55
+
56
+ interface StatusLike {
57
+ battery: number
58
+ charging: boolean
59
+ case: {battery: number; charging: boolean; open: boolean; removed: boolean}
60
+ signal: number
61
+ }
62
+
63
+ export function fixtureStatus<T extends StatusLike>(status: T, identity: IdentitySnapshot): T {
64
+ if (!fixtureActive(identity)) return status
65
+ return {
66
+ ...status,
67
+ battery: FIXTURE_GLASSES.battery,
68
+ charging: false,
69
+ case: {battery: FIXTURE_GLASSES.caseBattery, charging: false, open: false, removed: false},
70
+ signal: FIXTURE_GLASSES.signal,
71
+ }
72
+ }
73
+
74
+ interface InfoLike {
75
+ model: string
76
+ firmwareUpdateAvailable: boolean
77
+ }
78
+
79
+ export function fixtureInfo<T extends InfoLike>(info: T, identity: IdentitySnapshot): T {
80
+ if (!fixtureActive(identity)) return info
81
+ return {...info, model: FIXTURE_GLASSES.model, firmwareUpdateAvailable: false}
82
+ }
83
+
84
+ /** Fixture controllers first, then any real records that are not the fixture's. */
85
+ export function fixtureControllerList(list: PairedController[], identity: IdentitySnapshot): PairedController[] {
86
+ if (!fixtureActive(identity)) return list
87
+ const fixture = FIXTURE_CONTROLLERS.map(({id, model, name}) => ({id, model, name}))
88
+ const ids = new Set(fixture.map((record) => record.id))
89
+ return [...fixture, ...list.filter((record) => !ids.has(record.id))]
90
+ }
91
+
92
+ export function fixtureControllerStatuses(
93
+ statuses: Record<string, ControllerStatus>,
94
+ identity: IdentitySnapshot,
95
+ ): Record<string, ControllerStatus> {
96
+ if (!fixtureActive(identity)) return statuses
97
+ const result = {...statuses}
98
+ for (const {id, model, name, batteryLevel} of FIXTURE_CONTROLLERS) {
99
+ result[id] = {
100
+ id,
101
+ model,
102
+ name,
103
+ connected: true,
104
+ fullyBooted: true,
105
+ searching: false,
106
+ batteryLevel,
107
+ charging: false,
108
+ macAddress: "",
109
+ signalStrength: -62,
110
+ }
111
+ }
112
+ return result
113
+ }
@@ -0,0 +1,105 @@
1
+ import {MiniappErrorCode} from "@glassly/miniapp"
2
+
3
+ import type {ClientApp} from "../types/applet"
4
+
5
+ export type ActionError = {code: string; message: string} & Record<string, unknown>
6
+ export type ActionOutcome = {ok: true; data: unknown} | {ok: false; error: ActionError}
7
+
8
+ export const actionFailure = (code: string, message: string): ActionOutcome => ({ok: false, error: {code, message}})
9
+
10
+ /** Installed, compatible, and declares the action. */
11
+ export function appDeclaresAction(app: ClientApp | undefined, actionId: string): boolean {
12
+ if (!app) return false
13
+ if (app.compatibility && app.compatibility.isCompatible === false) return false
14
+ return (app.actions ?? []).some((a) => a.id === actionId)
15
+ }
16
+
17
+ /**
18
+ * Gates shared by miniapp->miniapp invoke and host invoke. Declared actions are
19
+ * public by default; `visibility: "system"` ones need a system caller.
20
+ */
21
+ export function checkActionAccess(
22
+ app: ClientApp | undefined,
23
+ target: string | undefined,
24
+ actionId: string | undefined,
25
+ callerIsSystem: boolean,
26
+ ): ActionError | null {
27
+ if (!target || !app) {
28
+ return {
29
+ code: MiniappErrorCode.APP_NOT_FOUND,
30
+ message: `Miniapp not found: ${target ?? "(missing targetPackageName)"}`,
31
+ }
32
+ }
33
+ if (!actionId) return {code: MiniappErrorCode.ACTION_NOT_FOUND, message: "invoke requires an actionId"}
34
+ if (app.compatibility && app.compatibility.isCompatible === false) {
35
+ return {
36
+ code: MiniappErrorCode.APP_NOT_COMPATIBLE,
37
+ message: `${target} is not compatible with the connected glasses`,
38
+ }
39
+ }
40
+ const declared = (app.actions ?? []).find((a) => a.id === actionId)
41
+ if (!declared) {
42
+ return {code: MiniappErrorCode.ACTION_NOT_FOUND, message: `${target} does not declare action "${actionId}"`}
43
+ }
44
+ if (declared.visibility === "system" && !callerIsSystem) {
45
+ return {
46
+ code: MiniappErrorCode.NOT_PERMITTED,
47
+ message: `Action "${actionId}" on ${target} is restricted to system apps`,
48
+ }
49
+ }
50
+ return null
51
+ }
52
+
53
+ /** BgTimer's shape; passed in so this module stays loadable without the native timer. */
54
+ export interface ActionTimers {
55
+ setTimeout: (cb: () => void, ms: number) => number
56
+ clearTimeout: (id: number) => void
57
+ }
58
+
59
+ /** Outstanding ACTION_CALLs awaiting an ACTION_RESULT, keyed by host-generated callId. */
60
+ export class PendingActionCalls {
61
+ private readonly calls = new Map<string, {target: string; settle: (o: ActionOutcome) => void; timer: number}>()
62
+
63
+ constructor(
64
+ private readonly timers: ActionTimers,
65
+ private readonly payloadTooLarge: (value: unknown) => boolean,
66
+ ) {}
67
+
68
+ /** Register a call; resolves with its outcome, or ACTION_TIMEOUT. */
69
+ register(callId: string, target: string, actionId: string, timeoutMs: number): Promise<ActionOutcome> {
70
+ return new Promise((resolve) => {
71
+ const timer = this.timers.setTimeout(() => {
72
+ this.calls.delete(callId)
73
+ resolve(actionFailure(MiniappErrorCode.ACTION_TIMEOUT, `action "${actionId}" timed out after ${timeoutMs}ms`))
74
+ }, timeoutMs)
75
+ this.calls.set(callId, {target, settle: resolve, timer})
76
+ })
77
+ }
78
+
79
+ /** Settle from a target's ACTION_RESULT payload. Only the invoked target may resolve a call. */
80
+ handleResult(targetPackageName: string, payload: Record<string, unknown>): void {
81
+ const callId = payload.callId as string | undefined
82
+ if (!callId) return
83
+ const pending = this.calls.get(callId)
84
+ // callIds are guessable (timestamp + seq): a different miniapp must not spoof someone else's result.
85
+ if (!pending || pending.target !== targetPackageName) return
86
+ this.calls.delete(callId)
87
+ this.timers.clearTimeout(pending.timer)
88
+
89
+ if (payload.ok === true) {
90
+ if (this.payloadTooLarge(payload.result)) {
91
+ pending.settle(actionFailure(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action result exceeded the 256 KB cap"))
92
+ return
93
+ }
94
+ pending.settle({ok: true, data: payload.result ?? null})
95
+ return
96
+ }
97
+ pending.settle({
98
+ ok: false,
99
+ error: (payload.error as ActionError | undefined) ?? {
100
+ code: MiniappErrorCode.INTERNAL,
101
+ message: "action handler error",
102
+ },
103
+ })
104
+ }
105
+ }