@glassly/engine 0.3.8-beta.984 → 0.3.8-dev.1000

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 (73) hide show
  1. package/build/engine.d.ts +2 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/ai.d.ts +3 -0
  4. package/build/facades/ai.d.ts.map +1 -1
  5. package/build/facades/ai.js +3 -0
  6. package/build/facades/ai.js.map +1 -1
  7. package/build/generated/releaseMetadata.js +3 -3
  8. package/build/generated/releaseMetadata.js.map +1 -1
  9. package/build/index.d.ts +1 -0
  10. package/build/index.d.ts.map +1 -1
  11. package/build/index.js.map +1 -1
  12. package/build/services/LocalDisplayManager.d.ts +0 -2
  13. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  14. package/build/services/LocalDisplayManager.js +5 -6
  15. package/build/services/LocalDisplayManager.js.map +1 -1
  16. package/build/services/LocalMiniappRuntime.d.ts +0 -6
  17. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  18. package/build/services/LocalMiniappRuntime.js +5 -23
  19. package/build/services/LocalMiniappRuntime.js.map +1 -1
  20. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  21. package/build/services/LocalSttFallbackCoordinator.js +2 -0
  22. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  23. package/build/services/LocalTranslationService.d.ts +2 -0
  24. package/build/services/LocalTranslationService.d.ts.map +1 -1
  25. package/build/services/LocalTranslationService.js +22 -0
  26. package/build/services/LocalTranslationService.js.map +1 -1
  27. package/build/services/OfflineActivity.d.ts +54 -0
  28. package/build/services/OfflineActivity.d.ts.map +1 -0
  29. package/build/services/OfflineActivity.js +86 -0
  30. package/build/services/OfflineActivity.js.map +1 -0
  31. package/build/services/STTModelManager.d.ts +1 -0
  32. package/build/services/STTModelManager.d.ts.map +1 -1
  33. package/build/services/STTModelManager.js +6 -0
  34. package/build/services/STTModelManager.js.map +1 -1
  35. package/build/services/SceneRenderer.d.ts +0 -8
  36. package/build/services/SceneRenderer.d.ts.map +1 -1
  37. package/build/services/SceneRenderer.js +0 -28
  38. package/build/services/SceneRenderer.js.map +1 -1
  39. package/build/services/controllers.d.ts.map +1 -1
  40. package/build/services/controllers.js +1 -0
  41. package/build/services/controllers.js.map +1 -1
  42. package/build/services/effectiveCapabilities.js +3 -1
  43. package/build/services/effectiveCapabilities.js.map +1 -1
  44. package/build/stores/settings.d.ts.map +1 -1
  45. package/build/stores/settings.js +7 -0
  46. package/build/stores/settings.js.map +1 -1
  47. package/build/types/capabilities/memomind-one.d.ts.map +1 -1
  48. package/build/types/capabilities/memomind-one.js +1 -0
  49. package/build/types/capabilities/memomind-one.js.map +1 -1
  50. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  51. package/build/types/capabilities/rayneo-io.js +3 -2
  52. package/build/types/capabilities/rayneo-io.js.map +1 -1
  53. package/build/types/enums.d.ts +2 -1
  54. package/build/types/enums.d.ts.map +1 -1
  55. package/build/types/enums.js +1 -0
  56. package/build/types/enums.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/facades/ai.ts +4 -0
  59. package/src/generated/releaseMetadata.ts +3 -3
  60. package/src/index.ts +8 -0
  61. package/src/services/LocalDisplayManager.ts +5 -7
  62. package/src/services/LocalMiniappRuntime.ts +4 -24
  63. package/src/services/LocalSttFallbackCoordinator.ts +4 -0
  64. package/src/services/LocalTranslationService.ts +20 -0
  65. package/src/services/OfflineActivity.ts +131 -0
  66. package/src/services/STTModelManager.ts +6 -0
  67. package/src/services/SceneRenderer.ts +0 -28
  68. package/src/services/controllers.ts +1 -0
  69. package/src/services/effectiveCapabilities.ts +3 -1
  70. package/src/stores/settings.ts +7 -0
  71. package/src/types/capabilities/memomind-one.ts +1 -0
  72. package/src/types/capabilities/rayneo-io.ts +3 -2
  73. package/src/types/enums.ts +1 -0
@@ -20,6 +20,7 @@ export declare enum ControllerTypes {
20
20
  APPLE_WATCH = "Apple Watch",
21
21
  WEAR_OS = "Wear OS Watch",
22
22
  GAMEBRICK = "IINE Gamebrick",
23
- IINE_ANANKE = "IINE Ananke Gen 2"
23
+ IINE_ANANKE = "IINE Ananke Gen 2",
24
+ ZERO2 = "8BitDo Zero 2"
24
25
  }
25
26
  //# sourceMappingURL=enums.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"enums.d.ts","sourceRoot":"","sources":["../../src/types/enums.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,wBAAwB,EAAC,MAAM,2BAA2B,CAAA;AAEhF,oBAAY,WAAW;IACrB,IAAI,SAAS;IACb,SAAS,sBAAsB;IAC/B,IAAI,SAAS;IACb,EAAE,sBAAsB;IACxB,EAAE,sBAAsB;IACxB,IAAI,gBAAgB;IACpB,KAAK,iBAAiB;IACtB,IAAI,eAAe;IACnB,GAAG,mBAAmB;IACtB,IAAI,SAAS;IACb,KAAK,oBAAoB;IACzB,SAAS,cAAc;IACvB,YAAY,iBAAiB;CAC9B;AAED,oBAAY,eAAe;IACzB,EAAE,sBAAsB;IACxB,WAAW,qBAAqB;IAChC,WAAW,gBAAgB;IAC3B,OAAO,kBAAkB;IACzB,SAAS,mBAAmB;IAC5B,WAAW,sBAAsB;CAClC"}
1
+ {"version":3,"file":"enums.d.ts","sourceRoot":"","sources":["../../src/types/enums.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,wBAAwB,EAAC,MAAM,2BAA2B,CAAA;AAEhF,oBAAY,WAAW;IACrB,IAAI,SAAS;IACb,SAAS,sBAAsB;IAC/B,IAAI,SAAS;IACb,EAAE,sBAAsB;IACxB,EAAE,sBAAsB;IACxB,IAAI,gBAAgB;IACpB,KAAK,iBAAiB;IACtB,IAAI,eAAe;IACnB,GAAG,mBAAmB;IACtB,IAAI,SAAS;IACb,KAAK,oBAAoB;IACzB,SAAS,cAAc;IACvB,YAAY,iBAAiB;CAC9B;AAED,oBAAY,eAAe;IACzB,EAAE,sBAAsB;IACxB,WAAW,qBAAqB;IAChC,WAAW,gBAAgB;IAC3B,OAAO,kBAAkB;IACzB,SAAS,mBAAmB;IAC5B,WAAW,sBAAsB;IACjC,KAAK,kBAAkB;CACxB"}
@@ -23,5 +23,6 @@ export var ControllerTypes;
23
23
  ControllerTypes["WEAR_OS"] = "Wear OS Watch";
24
24
  ControllerTypes["GAMEBRICK"] = "IINE Gamebrick";
25
25
  ControllerTypes["IINE_ANANKE"] = "IINE Ananke Gen 2";
26
+ ControllerTypes["ZERO2"] = "8BitDo Zero 2";
26
27
  })(ControllerTypes || (ControllerTypes = {}));
27
28
  //# sourceMappingURL=enums.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"enums.js","sourceRoot":"","sources":["../../src/types/enums.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,wBAAwB,EAAC,MAAM,2BAA2B,CAAA;AAEhF,MAAM,CAAN,IAAY,WAcX;AAdD,WAAY,WAAW;IACrB,4BAAa,CAAA;IACb,8CAA+B,CAAA;IAC/B,4BAAa,CAAA;IACb,uCAAwB,CAAA;IACxB,uCAAwB,CAAA;IACxB,mCAAoB,CAAA;IACpB,qCAAsB,CAAA;IACtB,kCAAmB,CAAA;IACnB,qCAAsB,CAAA;IACtB,4BAAa,CAAA;IACb,wCAAyB,CAAA;IACzB,sCAAuB,CAAA;IACvB,4CAA6B,CAAA;AAC/B,CAAC,EAdW,WAAW,KAAX,WAAW,QActB;AAED,MAAM,CAAN,IAAY,eAOX;AAPD,WAAY,eAAe;IACzB,2CAAwB,CAAA;IACxB,mDAAgC,CAAA;IAChC,8CAA2B,CAAA;IAC3B,4CAAyB,CAAA;IACzB,+CAA4B,CAAA;IAC5B,oDAAiC,CAAA;AACnC,CAAC,EAPW,eAAe,KAAf,eAAe,QAO1B","sourcesContent":["export {HardwareType, HardwareRequirementLevel} from \"@glassly/miniapp/hardware\"\n\nexport enum DeviceTypes {\n NONE = \"None\",\n SIMULATED = \"Simulated Glasses\",\n AR99 = \"AR99\",\n G1 = \"Even Realities G1\",\n G2 = \"Even Realities G2\",\n LIVE = \"Mentra Live\",\n MACH1 = \"Mentra Mach1\",\n Z100 = \"Vuzix Z100\",\n NEX = \"Mentra Display\",\n NIMO = \"NIMO\",\n FRAME = \"Brilliant Frame\",\n RAYNEO_IO = \"RayNeo iO\",\n MEMOMIND_ONE = \"MemoMind One\",\n}\n\nexport enum ControllerTypes {\n R1 = \"Even Realities R1\",\n NEURAL_BAND = \"Meta Neural Band\",\n APPLE_WATCH = \"Apple Watch\",\n WEAR_OS = \"Wear OS Watch\",\n GAMEBRICK = \"IINE Gamebrick\",\n IINE_ANANKE = \"IINE Ananke Gen 2\",\n}\n"]}
1
+ {"version":3,"file":"enums.js","sourceRoot":"","sources":["../../src/types/enums.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,wBAAwB,EAAC,MAAM,2BAA2B,CAAA;AAEhF,MAAM,CAAN,IAAY,WAcX;AAdD,WAAY,WAAW;IACrB,4BAAa,CAAA;IACb,8CAA+B,CAAA;IAC/B,4BAAa,CAAA;IACb,uCAAwB,CAAA;IACxB,uCAAwB,CAAA;IACxB,mCAAoB,CAAA;IACpB,qCAAsB,CAAA;IACtB,kCAAmB,CAAA;IACnB,qCAAsB,CAAA;IACtB,4BAAa,CAAA;IACb,wCAAyB,CAAA;IACzB,sCAAuB,CAAA;IACvB,4CAA6B,CAAA;AAC/B,CAAC,EAdW,WAAW,KAAX,WAAW,QActB;AAED,MAAM,CAAN,IAAY,eAQX;AARD,WAAY,eAAe;IACzB,2CAAwB,CAAA;IACxB,mDAAgC,CAAA;IAChC,8CAA2B,CAAA;IAC3B,4CAAyB,CAAA;IACzB,+CAA4B,CAAA;IAC5B,oDAAiC,CAAA;IACjC,0CAAuB,CAAA;AACzB,CAAC,EARW,eAAe,KAAf,eAAe,QAQ1B","sourcesContent":["export {HardwareType, HardwareRequirementLevel} from \"@glassly/miniapp/hardware\"\n\nexport enum DeviceTypes {\n NONE = \"None\",\n SIMULATED = \"Simulated Glasses\",\n AR99 = \"AR99\",\n G1 = \"Even Realities G1\",\n G2 = \"Even Realities G2\",\n LIVE = \"Mentra Live\",\n MACH1 = \"Mentra Mach1\",\n Z100 = \"Vuzix Z100\",\n NEX = \"Mentra Display\",\n NIMO = \"NIMO\",\n FRAME = \"Brilliant Frame\",\n RAYNEO_IO = \"RayNeo iO\",\n MEMOMIND_ONE = \"MemoMind One\",\n}\n\nexport enum ControllerTypes {\n R1 = \"Even Realities R1\",\n NEURAL_BAND = \"Meta Neural Band\",\n APPLE_WATCH = \"Apple Watch\",\n WEAR_OS = \"Wear OS Watch\",\n GAMEBRICK = \"IINE Gamebrick\",\n IINE_ANANKE = \"IINE Ananke Gen 2\",\n ZERO2 = \"8BitDo Zero 2\",\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.8-beta.984",
3
+ "version": "0.3.8-dev.1000",
4
4
  "description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,17 +97,17 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@glassly/bluetooth-sdk": "0.3.8-beta.984",
101
- "@glassly/cloud-client": "0.3.8-beta.984",
102
- "@glassly/cloud-protocol": "0.3.8-beta.984",
103
- "@glassly/crust": "0.3.8-beta.984",
104
- "@glassly/miniapp": "0.3.8-beta.984",
100
+ "@glassly/bluetooth-sdk": "0.3.8-dev.1000",
101
+ "@glassly/cloud-client": "0.3.8-dev.1000",
102
+ "@glassly/cloud-protocol": "0.3.8-dev.1000",
103
+ "@glassly/crust": "0.3.8-dev.1000",
104
+ "@glassly/miniapp": "0.3.8-dev.1000",
105
105
  "buffer": "^6.0.3",
106
106
  "events": "^3.3.0",
107
107
  "react-native-marked": "8.3.2",
108
108
  "semver": "^7.7.4",
109
109
  "typesafe-ts": "^1.7.1",
110
- "@glassly/llm-providers": "0.3.8-beta.984",
110
+ "@glassly/llm-providers": "0.3.8-dev.1000",
111
111
  "@huggingface/jinja": "^0.5.9",
112
112
  "jsonrepair": "^3.12.0"
113
113
  },
package/src/facades/ai.ts CHANGED
@@ -12,6 +12,7 @@ import localLlmModelManager, {
12
12
  type LocalLlmModelId,
13
13
  } from "../services/LocalLlmModelManager"
14
14
  import localLlmProvider from "../services/LocalLlmProvider"
15
+ import offlineActivity, {type OfflineActivity} from "../services/OfflineActivity"
15
16
 
16
17
  export const ai = {
17
18
  local: {
@@ -40,4 +41,7 @@ export const ai = {
40
41
  unload: (): Promise<void> => localLlmProvider.unload(),
41
42
  formatBytes: (bytes: number): string => LocalLlmModelManager.formatBytes(bytes),
42
43
  },
44
+ offlineActivity: (): OfflineActivity => offlineActivity.current(),
45
+ onOfflineActivity: (listener: (activity: OfflineActivity) => void): (() => void) =>
46
+ offlineActivity.subscribe(listener),
43
47
  }
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "0.3.8",
15
- "releaseIdentity": "0.3.8-beta.984",
16
- "releaseSetId": "glassly-0.3.8-beta.984",
17
- "sourceCommit": "845c08178406e394490afc747112557e5efa00e4",
15
+ "releaseIdentity": "0.3.8-dev.1000",
16
+ "releaseSetId": "glassly-0.3.8-dev.1000",
17
+ "sourceCommit": "8a287c091f98f631f77fe37b89eb6004ffc8ea1a",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
package/src/index.ts CHANGED
@@ -64,6 +64,14 @@ export type {
64
64
  LocalLlmModelId,
65
65
  } from "./services/LocalLlmModelManager"
66
66
  export {DEFAULT_LOCAL_LLM_MODEL, isLocalLlmModelId} from "./services/LocalLlmModelManager"
67
+ export type {
68
+ OfflineActivity,
69
+ OfflineActivityReason,
70
+ OfflineLlmActivity,
71
+ OfflineTranscriptionActivity,
72
+ OfflineTranslationActivity,
73
+ OfflineTranslationPair,
74
+ } from "./services/OfflineActivity"
67
75
 
68
76
  // Shared user memory timeline (engine.memory / Settings > Memory). Entry and
69
77
  // session shapes are canonical in @glassly/miniapp (the session.memory
@@ -489,11 +489,6 @@ class LocalDisplayManager {
489
489
  }
490
490
  }
491
491
 
492
- /** A frame for native to keep warm, never shown (SceneRenderer.prefetchScene). */
493
- public prefetch(packageName: string, view: "main" | "dashboard", elements: SceneElementInput[]): boolean {
494
- return sceneRenderer.prefetchScene(packageName, view, elements)
495
- }
496
-
497
492
  /**
498
493
  * The overlay slot only; bypasses arbitration and main ownership state.
499
494
  * Frames from an owner other than the slot's are dropped. Returns the scene
@@ -632,6 +627,9 @@ class LocalDisplayManager {
632
627
  this.currentDisplay &&
633
628
  this.currentDisplay.packageName === this.backgroundLock.packageName
634
629
  if (bgHoldsAndDisplays) {
630
+ // The retained scene is now older than the saved frame; forget it so
631
+ // the restore paints this frame rather than replaying the stale one.
632
+ sceneRenderer.clearApp(packageName)
635
633
  resolve?.({status: "blocked", reason: "a background app holds the display"})
636
634
  return
637
635
  }
@@ -979,8 +977,8 @@ class LocalDisplayManager {
979
977
  }, delay)
980
978
  }
981
979
  } else {
982
- // Retained state gone (app stopped since) — re-render the saved
983
- // elements as a fresh scene instead.
980
+ // Retained state gone (app stopped, or its newer frame was blocked) —
981
+ // re-render the saved elements as a fresh scene instead.
984
982
  this.sendNow(saved.packageName, {view: "main", scene: saved.scene, durationMs: remaining})
985
983
  }
986
984
  return
@@ -57,6 +57,7 @@ import type {AiTier, AiTierProvider, SavedLlmKeys} from "./AiTierResolution"
57
57
  import {decideLlmRoute, localLlmReason, type LocalLlmReason} from "./LlmRouting"
58
58
  import localLlmModelManager from "./LocalLlmModelManager"
59
59
  import localLlmProvider from "./LocalLlmProvider"
60
+ import offlineActivity from "./OfflineActivity"
60
61
  import {HttpError} from "@glassly/cloud-client"
61
62
  import type {
62
63
  QuotaStatusPayload,
@@ -1495,9 +1496,6 @@ class LocalMiniappRuntime {
1495
1496
  case MiniappRequestType.SHOW_VIEW:
1496
1497
  this.handleShowView(packageName, payload, requestId)
1497
1498
  break
1498
- case MiniappRequestType.PREFETCH:
1499
- this.handlePrefetch(packageName, payload, requestId)
1500
- break
1501
1499
  case MiniappRequestType.SETTINGS_GET:
1502
1500
  this.handleSettingsGet(packageName, payload, requestId)
1503
1501
  break
@@ -2654,27 +2652,6 @@ class LocalMiniappRuntime {
2654
2652
  )
2655
2653
  }
2656
2654
 
2657
- /**
2658
- * display.prefetch: the active dashboard hands over the frame its view would
2659
- * show if opened now. Native keeps its fonts and images on the glasses while
2660
- * nothing is on screen; nothing is displayed or retained here.
2661
- */
2662
- private handlePrefetch(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2663
- if (payload.view !== "dashboard" || !Array.isArray(payload.elements)) {
2664
- this.sendResult(packageName, requestId, false, undefined, {
2665
- code: MiniappErrorCode.INVALID_ARGUMENT,
2666
- message: 'prefetch takes the "dashboard" view and an elements array',
2667
- })
2668
- return
2669
- }
2670
- if (!this.connectedApps.has(packageName) || !isDashboardApp(packageName) || !isOverlayApp(packageName)) {
2671
- this.sendResult(packageName, requestId, true, {status: "unsupported", reason: "not the active dashboard"})
2672
- return
2673
- }
2674
- const sent = localDisplayManager.prefetch(packageName, "dashboard", payload.elements as NonNullable<DisplayPayload["scene"]>)
2675
- this.sendResult(packageName, requestId, true, sent ? {status: "queued"} : {status: "unsupported", reason: "no scene display"})
2676
- }
2677
-
2678
2655
  /**
2679
2656
  * Reject the request with PERMISSION_NOT_DECLARED unless the caller's
2680
2657
  * manifest declares `permission`. System apps pass without declaring it.
@@ -4367,6 +4344,7 @@ class LocalMiniappRuntime {
4367
4344
  return
4368
4345
  }
4369
4346
 
4347
+ const endActivity = offlineActivity.beginLlm(reason, localLlmModelManager.model(modelId).label)
4370
4348
  try {
4371
4349
  // webSearch is a vendor server-side grant; the phone has none.
4372
4350
  const {webSearch: _webSearch, ...localRequest} = request
@@ -4391,6 +4369,8 @@ class LocalMiniappRuntime {
4391
4369
  undefined,
4392
4370
  primaryError ?? {code: MiniappErrorCode.AI_LOCAL_UNAVAILABLE, message},
4393
4371
  )
4372
+ } finally {
4373
+ endActivity()
4394
4374
  }
4395
4375
  }
4396
4376
 
@@ -2,6 +2,7 @@ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
2
2
  import {ISLAND_SETTINGS_KEYS} from "../runtime/config"
3
3
  import {useSettingsStore} from "../stores/settings"
4
4
  import {cloudClientService} from "./CloudClientService"
5
+ import offlineActivity from "./OfflineActivity"
5
6
  import sttModelManager from "./STTModelManager"
6
7
 
7
8
  /**
@@ -223,6 +224,9 @@ class LocalSttFallbackCoordinator {
223
224
 
224
225
  private publishReason(): void {
225
226
  const reason = this.localActive ? this.currentReason() : ""
227
+ offlineActivity.setTranscription(
228
+ this.localActive ? {reason: this.currentReason(), model: sttModelManager.currentModelLabel()} : null,
229
+ )
226
230
  const store = useSettingsStore.getState()
227
231
  if (store.getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason) !== reason) {
228
232
  store.setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, reason)
@@ -1,3 +1,4 @@
1
+ import offlineActivity, {type ServedTranslationPair} from "./OfflineActivity"
1
2
  import {offlineTranslationManager} from "./OfflineTranslationManager"
2
3
 
3
4
  /** One concrete-target `translation:<source>:<target>` stream and who listens to it. */
@@ -67,6 +68,7 @@ export class LocalTranslationService {
67
68
 
68
69
  attach(host: Host): void {
69
70
  this.host = host
71
+ offlineActivity.setTranslationSource(() => this.servedPairs())
70
72
  }
71
73
 
72
74
  setSubscriptions(subscriptions: LocalTranslationSubscription[]): void {
@@ -75,6 +77,24 @@ export class LocalTranslationService {
75
77
  for (const key of this.lanes.keys()) {
76
78
  if (!live.has(key.substring(key.indexOf(">") + 1))) this.lanes.delete(key)
77
79
  }
80
+ offlineActivity.refresh()
81
+ }
82
+
83
+ servedPairs(): ServedTranslationPair[] {
84
+ if (!this.host || !offlineTranslationManager.isSupported()) return []
85
+ const pairs = new Map<string, ServedTranslationPair>()
86
+ let primaryUp: boolean | null = null
87
+ for (const sub of this.subscriptions) {
88
+ if (baseLanguage(sub.source) === baseLanguage(sub.target)) continue
89
+ if (!sub.forceLocal) {
90
+ primaryUp ??= this.host.primaryUp()
91
+ if (primaryUp || !sub.cloud) continue
92
+ }
93
+ const key = `${sub.source}>${sub.target}`
94
+ const existing = pairs.get(key)
95
+ pairs.set(key, {source: sub.source, target: sub.target, forceLocal: sub.forceLocal || !!existing?.forceLocal})
96
+ }
97
+ return [...pairs.values()]
78
98
  }
79
99
 
80
100
  onLocalTranscript(transcript: LocalTranscript, sourceLanguage: string): void {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * What the phone is running on-device right now (transcription, translation,
3
+ * LLM) and why, for the home offline card. Producers push in; it imports
4
+ * nothing at runtime so their test stubs stay unaffected.
5
+ */
6
+ import type {LocalLlmReason} from "./LlmRouting"
7
+ import type {LocalSttFallbackReason} from "./LocalSttFallbackCoordinator"
8
+
9
+ export type OfflineActivityReason = LocalSttFallbackReason | LocalLlmReason
10
+
11
+ export interface OfflineTranscriptionActivity {
12
+ reason: LocalSttFallbackReason
13
+ model: string
14
+ }
15
+
16
+ export interface OfflineTranslationPair {
17
+ source: string
18
+ target: string
19
+ }
20
+
21
+ export interface OfflineTranslationActivity {
22
+ reason: OfflineActivityReason
23
+ pairs: OfflineTranslationPair[]
24
+ }
25
+
26
+ export interface OfflineLlmActivity {
27
+ reason: LocalLlmReason
28
+ model: string
29
+ requests: number
30
+ }
31
+
32
+ export interface OfflineActivity {
33
+ transcription: OfflineTranscriptionActivity | null
34
+ translation: OfflineTranslationActivity | null
35
+ llm: OfflineLlmActivity | null
36
+ }
37
+
38
+ export interface ServedTranslationPair extends OfflineTranslationPair {
39
+ forceLocal: boolean
40
+ }
41
+
42
+ const EMPTY: OfflineActivity = {transcription: null, translation: null, llm: null}
43
+
44
+ class OfflineActivityTracker {
45
+ private transcription: OfflineTranscriptionActivity | null = null
46
+ private llmRuns: Array<{reason: LocalLlmReason; model: string}> = []
47
+ private translationSource: (() => ServedTranslationPair[]) | null = null
48
+ private snapshot: OfflineActivity = EMPTY
49
+ private snapshotKey = JSON.stringify(EMPTY)
50
+ private listeners = new Set<(activity: OfflineActivity) => void>()
51
+
52
+ current(): OfflineActivity {
53
+ return this.snapshot
54
+ }
55
+
56
+ subscribe(listener: (activity: OfflineActivity) => void): () => void {
57
+ this.listeners.add(listener)
58
+ return () => {
59
+ this.listeners.delete(listener)
60
+ }
61
+ }
62
+
63
+ setTranscription(transcription: OfflineTranscriptionActivity | null): void {
64
+ this.transcription = transcription
65
+ this.refresh()
66
+ }
67
+
68
+ beginLlm(reason: LocalLlmReason, model: string): () => void {
69
+ const run = {reason, model}
70
+ this.llmRuns.push(run)
71
+ this.refresh()
72
+ let ended = false
73
+ return () => {
74
+ if (ended) return
75
+ ended = true
76
+ this.llmRuns = this.llmRuns.filter((r) => r !== run)
77
+ this.refresh()
78
+ }
79
+ }
80
+
81
+ setTranslationSource(source: (() => ServedTranslationPair[]) | null): void {
82
+ this.translationSource = source
83
+ this.refresh()
84
+ }
85
+
86
+ refresh(): void {
87
+ const next: OfflineActivity = {
88
+ transcription: this.transcription,
89
+ translation: this.translation(),
90
+ llm: this.llm(),
91
+ }
92
+ const key = JSON.stringify(next)
93
+ if (key === this.snapshotKey) return
94
+ this.snapshotKey = key
95
+ this.snapshot = next
96
+ for (const listener of this.listeners) {
97
+ try {
98
+ listener(next)
99
+ } catch (error) {
100
+ console.warn("OfflineActivity: listener failed:", error)
101
+ }
102
+ }
103
+ }
104
+
105
+ reset(): void {
106
+ this.transcription = null
107
+ this.llmRuns = []
108
+ this.translationSource = null
109
+ this.refresh()
110
+ }
111
+
112
+ private translation(): OfflineTranslationActivity | null {
113
+ if (!this.transcription || !this.translationSource) return null
114
+ const pairs = this.translationSource()
115
+ if (pairs.length === 0) return null
116
+ return {
117
+ reason: pairs.some((p) => p.forceLocal) ? "force_local" : this.transcription.reason,
118
+ pairs: pairs.map(({source, target}) => ({source, target})),
119
+ }
120
+ }
121
+
122
+ private llm(): OfflineLlmActivity | null {
123
+ const latest = this.llmRuns[this.llmRuns.length - 1]
124
+ if (!latest) return null
125
+ return {reason: latest.reason, model: latest.model, requests: this.llmRuns.length}
126
+ }
127
+ }
128
+
129
+ export {OfflineActivityTracker}
130
+ const offlineActivity = new OfflineActivityTracker()
131
+ export default offlineActivity
@@ -228,6 +228,12 @@ class STTModelManager {
228
228
  return this.currentLanguage
229
229
  }
230
230
 
231
+ currentModelLabel(): string {
232
+ const language = this.config(this.currentLanguage)
233
+ if (!language) return this.currentLanguage
234
+ return language.type === "apple" ? `Apple · ${language.displayName}` : language.displayName
235
+ }
236
+
231
237
  setCurrentLanguage(code: string): void {
232
238
  if (this.config(code)) {
233
239
  this.currentLanguage = code
@@ -283,34 +283,6 @@ class SceneRenderer {
283
283
  }
284
284
  }
285
285
 
286
- /**
287
- * Hand native the frame `elements` would make for (app, view) without
288
- * showing it or touching the retained scene: drivers with an on-glass asset
289
- * cache upload its fonts and images while the panel is idle. Every element
290
- * is "created" and the frame carries seq 0 (no presentation report). Empty
291
- * elements drop it. False when there is nothing to send it to.
292
- */
293
- public prefetchScene(appId: string, view: "main" | "dashboard", elements: SceneElementInput[]): boolean {
294
- const caps = this.currentCapabilities()
295
- if (!caps?.canPosition) return false
296
- const processed = processScene(resolveAnchors(elements, caps.width, caps.height), caps, displayProcessor.getSceneProfile())
297
- let synthetic = 0
298
- const {elements: created} = diffScene([], processed.elements, () => `~prefetch${synthetic++}`)
299
- const frame: SceneFrame = {
300
- appId,
301
- view,
302
- sceneEpoch: 0,
303
- seq: 0,
304
- elements: created,
305
- removed: [],
306
- canvas: {width: caps.width, height: caps.height},
307
- }
308
- void Promise.resolve(BluetoothSdk.displayEvent({view, scene: frame, prefetch: true})).catch((err) => {
309
- console.error(`${LOG_TAG}: native prefetch failed:`, err)
310
- })
311
- return true
312
- }
313
-
314
286
  /**
315
287
  * Re-emit an app's retained scene (reconnect / arbitration restore). Replay
316
288
  * is always create-based — the device rebuilds from scratch (spec §4).
@@ -55,6 +55,7 @@ const INPUT_SURFACES: Readonly<Record<string, readonly InputSurface[]>> = {
55
55
  [ControllerTypes.WEAR_OS]: [WATCH_SURFACE],
56
56
  [ControllerTypes.GAMEBRICK]: [GAMEBRICK_SURFACE],
57
57
  [ControllerTypes.IINE_ANANKE]: [GAMEBRICK_SURFACE],
58
+ [ControllerTypes.ZERO2]: [GAMEBRICK_SURFACE],
58
59
  [ControllerTypes.R1]: [],
59
60
  }
60
61
 
@@ -61,6 +61,8 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
61
61
  // Glassly CFW supports the complete vector contract, including SVG and rotation.
62
62
  // Upstream g2flash publishes a raster canvas with sceneVector=false.
63
63
  const vector = useGlassesStore.getState().sceneVector === true
64
+ // Depth is a disparity between the lenses; one panel (MemoMind One) draws every element flat.
65
+ const stereo = (base.display.count ?? 1) >= 2
64
66
  return {
65
67
  ...(volumeGate ? withVolumeGate(base) : base),
66
68
  hasNativeNavigation: resolveNativeNavigation(base.hasNativeNavigation, true, forceNativeNavigationOnCfw()),
@@ -81,7 +83,7 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
81
83
  vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_IMAGE_ELEMENTS,
82
84
  ),
83
85
  ...(vector
84
- ? {shapes: [...ALL_DISPLAY_SHAPES, "svg" as const], animation: true, rotation: true, depth: true}
86
+ ? {shapes: [...ALL_DISPLAY_SHAPES, "svg" as const], animation: true, rotation: true, depth: stereo}
85
87
  : {}),
86
88
  },
87
89
  }
@@ -930,6 +930,13 @@ export const SETTINGS: Record<string, Setting> = {
930
930
  saveOnServer: false,
931
931
  persist: true,
932
932
  },
933
+ home_offline_card: {
934
+ key: "home_offline_card",
935
+ defaultValue: () => true,
936
+ writable: true,
937
+ saveOnServer: false,
938
+ persist: true,
939
+ },
933
940
  // Gesture-dots debug indicator: flash a glyph on the glasses when a touch
934
941
  // gesture is received while the display is empty. Works on any
935
942
  // scene-capable device, not only CFW. Device-local debug knob, default off.
@@ -6,6 +6,7 @@ export const memoMindOne: Capabilities = {
6
6
  camera: null,
7
7
  hasDisplay: true,
8
8
  display: {
9
+ // One panel: the vector canvas offers no depth on it.
9
10
  count: 1,
10
11
  isColor: false,
11
12
  canDisplayBitmap: true,
@@ -19,10 +19,11 @@ export const rayneoIO: Capabilities = {
19
19
  hasCamera: false,
20
20
  camera: null,
21
21
 
22
- // Display capabilities - single monochrome (1-bit) panel
22
+ // Display capabilities - a monochrome panel per eye; the Glassly CFW keeps
23
+ // a scene per lens, so element depth is a real disparity here
23
24
  hasDisplay: true,
24
25
  display: {
25
- count: 1,
26
+ count: 2,
26
27
  isColor: false,
27
28
  color: "green",
28
29
  // Bitmaps and positioned elements need the Glassly CFW; stock firmware
@@ -23,4 +23,5 @@ export enum ControllerTypes {
23
23
  WEAR_OS = "Wear OS Watch",
24
24
  GAMEBRICK = "IINE Gamebrick",
25
25
  IINE_ANANKE = "IINE Ananke Gen 2",
26
+ ZERO2 = "8BitDo Zero 2",
26
27
  }