@glassly/engine 0.4.0-beta.1059 → 0.4.0-beta.1076

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 (54) hide show
  1. package/build/engine.d.ts +1 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/permissions.d.ts.map +1 -1
  4. package/build/facades/permissions.js +0 -2
  5. package/build/facades/permissions.js.map +1 -1
  6. package/build/facades/phoneNotifications.d.ts +7 -0
  7. package/build/facades/phoneNotifications.d.ts.map +1 -1
  8. package/build/facades/phoneNotifications.js +6 -0
  9. package/build/facades/phoneNotifications.js.map +1 -1
  10. package/build/generated/releaseMetadata.js +3 -3
  11. package/build/generated/releaseMetadata.js.map +1 -1
  12. package/build/index.d.ts +1 -0
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js.map +1 -1
  15. package/build/services/LocalLlmModelManager.d.ts.map +1 -1
  16. package/build/services/LocalLlmModelManager.js +4 -3
  17. package/build/services/LocalLlmModelManager.js.map +1 -1
  18. package/build/services/LocalLlmProvider.d.ts.map +1 -1
  19. package/build/services/LocalLlmProvider.js +5 -6
  20. package/build/services/LocalLlmProvider.js.map +1 -1
  21. package/build/services/LocalMiniappRuntime.d.ts +3 -0
  22. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  23. package/build/services/LocalMiniappRuntime.js +67 -1
  24. package/build/services/LocalMiniappRuntime.js.map +1 -1
  25. package/build/services/notifications/miniappNotifications.d.ts +31 -0
  26. package/build/services/notifications/miniappNotifications.d.ts.map +1 -0
  27. package/build/services/notifications/miniappNotifications.js +24 -0
  28. package/build/services/notifications/miniappNotifications.js.map +1 -0
  29. package/build/services/pages/pageEligibility.d.ts +1 -2
  30. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  31. package/build/services/pages/pageEligibility.js +4 -2
  32. package/build/services/pages/pageEligibility.js.map +1 -1
  33. package/build/services/uiShim.d.ts.map +1 -1
  34. package/build/services/uiShim.js +14 -0
  35. package/build/services/uiShim.js.map +1 -1
  36. package/build/stores/settings.d.ts.map +1 -1
  37. package/build/stores/settings.js +7 -8
  38. package/build/stores/settings.js.map +1 -1
  39. package/build/types/capabilities/nimo.d.ts.map +1 -1
  40. package/build/types/capabilities/nimo.js +2 -0
  41. package/build/types/capabilities/nimo.js.map +1 -1
  42. package/package.json +15 -15
  43. package/src/facades/permissions.ts +0 -2
  44. package/src/facades/phoneNotifications.ts +10 -0
  45. package/src/generated/releaseMetadata.ts +3 -3
  46. package/src/index.ts +1 -0
  47. package/src/services/LocalLlmModelManager.ts +4 -3
  48. package/src/services/LocalLlmProvider.ts +6 -7
  49. package/src/services/LocalMiniappRuntime.ts +79 -1
  50. package/src/services/notifications/miniappNotifications.ts +53 -0
  51. package/src/services/pages/pageEligibility.ts +3 -2
  52. package/src/services/uiShim.ts +14 -0
  53. package/src/stores/settings.ts +7 -8
  54. package/src/types/capabilities/nimo.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.4.0-beta.1059",
3
+ "version": "0.4.0-beta.1076",
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,26 +97,26 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@glassly/bluetooth-sdk": "0.4.0-beta.1059",
101
- "@glassly/cloud-client": "0.4.0-beta.1059",
102
- "@glassly/cloud-protocol": "0.4.0-beta.1059",
103
- "@glassly/crust": "0.4.0-beta.1059",
104
- "@glassly/miniapp": "0.4.0-beta.1059",
100
+ "@glassly/bluetooth-sdk": "0.4.0-beta.1076",
101
+ "@glassly/cloud-client": "0.4.0-beta.1076",
102
+ "@glassly/cloud-protocol": "0.4.0-beta.1076",
103
+ "@glassly/crust": "0.4.0-beta.1076",
104
+ "@glassly/miniapp": "0.4.0-beta.1076",
105
105
  "buffer": "^6.0.3",
106
106
  "events": "^3.3.0",
107
- "react-native-marked": "8.3.2",
108
- "semver": "^7.7.4",
107
+ "react-native-marked": "8.3.4",
108
+ "semver": "^7.8.6",
109
109
  "typesafe-ts": "^1.7.1",
110
- "@glassly/llm-providers": "0.4.0-beta.1059",
111
- "@huggingface/jinja": "^0.5.9",
112
- "jsonrepair": "^3.12.0"
110
+ "@glassly/llm-providers": "0.4.0-beta.1076",
111
+ "@huggingface/jinja": "^0.5.12",
112
+ "jsonrepair": "^3.15.0"
113
113
  },
114
114
  "devDependencies": {
115
115
  "@glassly/llm-providers": "workspace:*",
116
- "@types/semver": "^7.7.1",
116
+ "@types/semver": "^7.8.0",
117
117
  "expo-module-scripts": "^56.0.3",
118
- "expo-modules-core": "^58.0.23",
119
- "react-native-nitro-modules": "^0.37.1",
118
+ "expo-modules-core": "^58.0.30",
119
+ "react-native-nitro-modules": "^0.37.2",
120
120
  "typescript": "~6.0.3"
121
121
  },
122
122
  "peerDependencies": {
@@ -155,7 +155,7 @@
155
155
  "@dr.pogodin/react-native-fs": "*",
156
156
  "@craftzdog/react-native-buffer": "*",
157
157
  "axios": "*",
158
- "react-native-executorch": ">=0.10.5 <0.11",
158
+ "react-native-executorch": ">=0.11.0 <0.12",
159
159
  "react-native-blob-util": "*"
160
160
  }
161
161
  }
@@ -125,14 +125,12 @@ export const permissions = {
125
125
  features.add(PermissionFeatures.CALENDAR)
126
126
  features.add(PermissionFeatures.LOCATION)
127
127
  features.add(PermissionFeatures.BACKGROUND_LOCATION)
128
- features.add(PermissionFeatures.POST_NOTIFICATIONS)
129
128
  features.add("read_notifications")
130
129
  break
131
130
  case "MICROPHONE":
132
131
  case "CALENDAR":
133
132
  case "LOCATION":
134
133
  case "BACKGROUND_LOCATION":
135
- case "POST_NOTIFICATIONS":
136
134
  features.add(PermissionFeatures[permission.type])
137
135
  break
138
136
  case "READ_NOTIFICATIONS":
@@ -23,6 +23,9 @@ import {
23
23
  nativeNotificationsSupported,
24
24
  uploadPhoneNotification,
25
25
  } from "../services/PhoneNotificationsSync"
26
+ import {onMiniappNotification, type MiniappNotificationEvent} from "../services/notifications/miniappNotifications"
27
+
28
+ export type {MiniappNotificationEvent} from "../services/notifications/miniappNotifications"
26
29
 
27
30
  export const phoneNotifications = {
28
31
  /**
@@ -65,6 +68,13 @@ export const phoneNotifications = {
65
68
  * surface runs and draw only when the answer is `host`.
66
69
  */
67
70
  presentationOwner: currentPresentationOwner,
71
+ /**
72
+ * Notifications miniapps post and dismiss. Hosts feed them to the same
73
+ * fan-out as the phone's, marked `system: false` with `source` set.
74
+ */
75
+ onMiniappNotification: (listener: (event: MiniappNotificationEvent) => void): (() => void) =>
76
+ onMiniappNotification(listener),
77
+
68
78
  /** Hand a captured notification to firmware that presents uploaded content (owner `firmware`, Android). */
69
79
  uploadToGlasses: (event: PhoneNotificationEvent): Promise<void> => uploadPhoneNotification(event),
70
80
 
@@ -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.4.0",
15
- "releaseIdentity": "0.4.0-beta.1059",
16
- "releaseSetId": "glassly-0.4.0-beta.1059",
17
- "sourceCommit": "23003d12f55878215f018b868e826bb6516c556a",
15
+ "releaseIdentity": "0.4.0-beta.1076",
16
+ "releaseSetId": "glassly-0.4.0-beta.1076",
17
+ "sourceCommit": "3021c69f679562e3de8a57d76c806d1c8173c22a",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export type {
23
23
  } from "./facades/reports"
24
24
  export type {IslandNotification, IslandNotificationKind} from "./facades/notifications"
25
25
  export type {PresentationOwner} from "./services/notifications/notificationPolicy"
26
+ export type {MiniappNotificationEvent} from "./services/notifications/miniappNotifications"
26
27
  export {DEFAULT_HIDDEN_PACKAGES, isGatedByDevTools, isSystemMiniapp} from "./stores/appVisibility"
27
28
  export type {WifiSearchResult} from "./facades/glassesWifi"
28
29
  export type {IdentitySnapshot} from "./services/PairingIdentity"
@@ -56,9 +56,10 @@ export interface LocalLlmDownloadStatus {
56
56
  }
57
57
 
58
58
  // XNNPACK 8da4w exports from software-mansion/react-native-executorch-qwen-3,
59
- // pinned to the v0.10.0 release tag that react-native-executorch 0.10.x
60
- // expects. modelBytes are the .pte sizes on the Hub; sizeBytes adds the
61
- // tokenizer files (~11 MB).
59
+ // pinned to the v0.10.0 release tag. They export every method the 0.11 text
60
+ // runner validates; moving to the v0.11.0 re-exports changes the file sizes and
61
+ // makes every existing download re-fetch. modelBytes are the .pte sizes on the
62
+ // Hub; sizeBytes adds the tokenizer files (~11 MB).
62
63
  const QWEN3_BASE = "https://huggingface.co/software-mansion/react-native-executorch-qwen-3/resolve/v0.10.0"
63
64
  const TOKENIZER_BYTES = 11_431_080
64
65
  const MB = 1024 * 1024
@@ -33,7 +33,7 @@ type Rne = typeof import("react-native-executorch")
33
33
  // The runner API lives on the `llm` namespace; derive its types from there so
34
34
  // the import stays a type-only reference (no native module at load time).
35
35
  type Tokenizer = ReturnType<Rne["nlp"]["loadTokenizer"]>
36
- type LLMRunner = ReturnType<Rne["llm"]["createLLMRunner"]>
36
+ type LLMRunner = Awaited<ReturnType<Rne["llm"]["createLLMRunner"]>>
37
37
  type LLMGenerationStats = ReturnType<LLMRunner["generate"]>
38
38
  type LLMGenerationConfig = NonNullable<Parameters<LLMRunner["generate"]>[1]>
39
39
 
@@ -238,15 +238,15 @@ class LocalLlmProvider {
238
238
 
239
239
  const elapsedMs = Date.now() - startedAt
240
240
  console.log(
241
- `${LOG_TAG}: ${model.modelId} prompt=${generated.stats.numPromptTokens} out=${generated.stats.numGeneratedTokens} tokens in ${elapsedMs} ms`,
241
+ `${LOG_TAG}: ${model.modelId} prompt=${generated.stats.prefill.numTokens} out=${generated.stats.numTokens} tokens in ${elapsedMs} ms`,
242
242
  )
243
243
 
244
244
  const seed = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
245
245
  const completion = finalizeLocalResult(
246
246
  {
247
247
  text: generated.text,
248
- promptTokens: generated.stats.numPromptTokens,
249
- generatedTokens: generated.stats.numGeneratedTokens,
248
+ promptTokens: generated.stats.prefill.numTokens,
249
+ generatedTokens: generated.stats.numTokens,
250
250
  maxNewTokens,
251
251
  },
252
252
  {jsonMode, toolsOffered: Boolean(tools), makeToolCallId: (index) => `local_${seed}_${index}`},
@@ -290,11 +290,10 @@ class LocalLlmProvider {
290
290
  const template = new Template(tokenizerConfig.chatTemplate)
291
291
 
292
292
  const startedAt = Date.now()
293
- const createRunner = rne.wrapAsync(rne.llm.createLLMRunner)
294
- const runner = await createRunner(paths.model, paths.tokenizer, [])
293
+ const runner = await rne.llm.createLLMRunner(paths.model, paths.tokenizer, [])
295
294
  let tokenizer: Tokenizer | undefined
296
295
  try {
297
- const maxSeqLen = runner.getKVCacheState().maxSeqLen
296
+ const maxSeqLen = runner.getKVCacheState().maxContextLen
298
297
  if (!Number.isInteger(maxSeqLen) || maxSeqLen <= CONTEXT_MARGIN_TOKENS + 16) {
299
298
  throw new Error(`On-device model ${modelId} reported an invalid context size: ${maxSeqLen}`)
300
299
  }
@@ -83,6 +83,13 @@ import overlayPages from "./pages/OverlayPages"
83
83
  import dashboardOpenTrace from "./debug/DashboardOpenTrace"
84
84
  import {isAppForegrounded, isPhoneActive, onAppForegroundChange, onPhoneActivityChange} from "./PhoneActivity"
85
85
  import {installedNotificationApps} from "./PhoneNotificationsSync"
86
+ import {
87
+ emitMiniappNotification,
88
+ MINIAPP_NOTIFICATION_CONTENT_MAX,
89
+ MINIAPP_NOTIFICATION_ID_MAX,
90
+ MINIAPP_NOTIFICATION_TITLE_MAX,
91
+ miniappNotificationId,
92
+ } from "./notifications/miniappNotifications"
86
93
  import {MiniappIcons} from "./MiniappIcons"
87
94
  import {pairedControllersFrom, readyControllersKey} from "./controllers"
88
95
  import {
@@ -1513,6 +1520,12 @@ class LocalMiniappRuntime {
1513
1520
  case MiniappRequestType.NOTIFICATION_APPS_LIST:
1514
1521
  void this.handleNotificationAppsList(packageName, requestId)
1515
1522
  break
1523
+ case MiniappRequestType.NOTIFICATION_POST:
1524
+ this.handleNotificationPost(packageName, payload, requestId)
1525
+ break
1526
+ case MiniappRequestType.NOTIFICATION_DISMISS:
1527
+ this.handleNotificationDismiss(packageName, payload, requestId)
1528
+ break
1516
1529
  case MiniappRequestType.NATIVE_NAV:
1517
1530
  this.handleNativeNav(packageName, payload, requestId)
1518
1531
  break
@@ -2792,7 +2805,17 @@ class LocalMiniappRuntime {
2792
2805
  return
2793
2806
  }
2794
2807
  try {
2795
- this.sendResult(packageName, requestId, true, await installedNotificationApps())
2808
+ const posters = this.interopApps().filter(
2809
+ (app) => app.packageName && app.permissions?.some((permission) => permission.type === "POST_NOTIFICATIONS"),
2810
+ )
2811
+ const miniapps = await Promise.all(
2812
+ posters.map(async (app) => ({
2813
+ packageName: app.packageName,
2814
+ appName: app.name || app.packageName,
2815
+ icon: (await this.miniappIcons.read(app)) ?? null,
2816
+ })),
2817
+ )
2818
+ this.sendResult(packageName, requestId, true, [...(await installedNotificationApps()), ...miniapps])
2796
2819
  } catch (err) {
2797
2820
  this.sendResult(packageName, requestId, false, undefined, {
2798
2821
  code: MiniappErrorCode.INTERNAL,
@@ -2801,6 +2824,60 @@ class LocalMiniappRuntime {
2801
2824
  }
2802
2825
  }
2803
2826
 
2827
+ private handleNotificationPost(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2828
+ if (!this.requirePermission(packageName, requestId, "POST_NOTIFICATIONS", "to post notifications")) return
2829
+ const title = typeof payload.title === "string" ? payload.title.slice(0, MINIAPP_NOTIFICATION_TITLE_MAX) : ""
2830
+ const content =
2831
+ typeof payload.content === "string" ? payload.content.slice(0, MINIAPP_NOTIFICATION_CONTENT_MAX) : ""
2832
+ if (!title.trim() && !content.trim()) {
2833
+ this.sendResult(packageName, requestId, false, undefined, {
2834
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2835
+ message: "A notification needs a title or content",
2836
+ })
2837
+ return
2838
+ }
2839
+ if (payload.id !== undefined && (typeof payload.id !== "string" || !payload.id.trim())) {
2840
+ this.sendResult(packageName, requestId, false, undefined, {
2841
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2842
+ message: "A notification id must be a non-empty string",
2843
+ })
2844
+ return
2845
+ }
2846
+ const id =
2847
+ typeof payload.id === "string"
2848
+ ? payload.id.trim().slice(0, MINIAPP_NOTIFICATION_ID_MAX)
2849
+ : `${Date.now().toString(36)}-${++this.notificationSeq}`
2850
+ const app = this.interopApps().find((candidate) => candidate.packageName === packageName)
2851
+ emitMiniappNotification({
2852
+ type: "posted",
2853
+ notificationId: miniappNotificationId(packageName, id),
2854
+ source: packageName,
2855
+ app: app?.name || packageName,
2856
+ title,
2857
+ content,
2858
+ timestamp: Date.now(),
2859
+ })
2860
+ this.sendResult(packageName, requestId, true, {id})
2861
+ }
2862
+
2863
+ private handleNotificationDismiss(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2864
+ if (!this.requirePermission(packageName, requestId, "POST_NOTIFICATIONS", "to dismiss notifications")) return
2865
+ if (typeof payload.id !== "string" || !payload.id.trim()) {
2866
+ this.sendResult(packageName, requestId, false, undefined, {
2867
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2868
+ message: "dismiss needs the id post() returned",
2869
+ })
2870
+ return
2871
+ }
2872
+ emitMiniappNotification({
2873
+ type: "dismissed",
2874
+ notificationId: miniappNotificationId(packageName, payload.id.trim().slice(0, MINIAPP_NOTIFICATION_ID_MAX)),
2875
+ source: packageName,
2876
+ timestamp: Date.now(),
2877
+ })
2878
+ this.sendResult(packageName, requestId, true)
2879
+ }
2880
+
2804
2881
  /**
2805
2882
  * Push bridged settings as they change: stored keys from the store, the
2806
2883
  * derived keys whenever their inputs move (phone activity, the display,
@@ -6269,6 +6346,7 @@ class LocalMiniappRuntime {
6269
6346
  }
6270
6347
 
6271
6348
  private miniappIcons = new MiniappIcons()
6349
+ private notificationSeq = 0
6272
6350
 
6273
6351
  private async handleMiniappsList(
6274
6352
  packageName: string,
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Notifications miniapps post (`session.phone.notifications.post`) on their
3
+ * way into the host's phone-notification path. The runtime validates and
4
+ * emits here; the host subscribes through `engine.phoneNotifications` and
5
+ * feeds them to the same fan-out as the phone's own, with `system: false`
6
+ * and `source` set, so the dashboard, other readers and presentation treat
7
+ * them alike.
8
+ */
9
+ export interface MiniappNotificationPosted {
10
+ type: "posted"
11
+ notificationId: string
12
+ source: string
13
+ app: string
14
+ title: string
15
+ content: string
16
+ timestamp: number
17
+ }
18
+
19
+ export interface MiniappNotificationDismissed {
20
+ type: "dismissed"
21
+ notificationId: string
22
+ source: string
23
+ timestamp: number
24
+ }
25
+
26
+ export type MiniappNotificationEvent = MiniappNotificationPosted | MiniappNotificationDismissed
27
+
28
+ export const MINIAPP_NOTIFICATION_TITLE_MAX = 200
29
+ export const MINIAPP_NOTIFICATION_CONTENT_MAX = 2_000
30
+ export const MINIAPP_NOTIFICATION_ID_MAX = 128
31
+
32
+ const listeners = new Set<(event: MiniappNotificationEvent) => void>()
33
+
34
+ export function miniappNotificationId(source: string, id: string): string {
35
+ return `${source}:${id}`
36
+ }
37
+
38
+ export function emitMiniappNotification(event: MiniappNotificationEvent): void {
39
+ for (const listener of listeners) {
40
+ try {
41
+ listener(event)
42
+ } catch (err) {
43
+ console.warn(`miniappNotifications: listener threw: ${(err as Error)?.message ?? err}`)
44
+ }
45
+ }
46
+ }
47
+
48
+ export function onMiniappNotification(listener: (event: MiniappNotificationEvent) => void): () => void {
49
+ listeners.add(listener)
50
+ return () => {
51
+ listeners.delete(listener)
52
+ }
53
+ }
@@ -21,11 +21,12 @@ export function isCfwFirmwareVersion(firmwareVersion: string | null | undefined)
21
21
 
22
22
  /**
23
23
  * Only models that ship a Glassly custom firmware, and only while they run it.
24
- * Stock firmware on those models and every other model (NIMO, G1, ...) are
25
- * intentionally excluded.
24
+ * Stock firmware and every other model are intentionally excluded.
26
25
  */
27
26
  export function isCustomDashboardEligible(info: {model?: string | null; firmwareVersion?: string | null}): boolean {
28
27
  if (!info.model) return false
28
+ // The NIMO drivers draw no scenes through its CFW yet; the dashboard would land on the text page.
29
+ if (info.model === DeviceTypes.NIMO) return false
29
30
  if (info.model === DeviceTypes.MEMOMIND_ONE) {
30
31
  const match = /^GLASSLYCFW\/([0-9]+)$/.exec(info.firmwareVersion ?? "")
31
32
  const revision = match ? Number(match[1]) : 0
@@ -304,6 +304,13 @@ export function buildUiShim(options: UiShimOptions): string {
304
304
  postEnvelope({ type: 'set_background', color: color });
305
305
  }
306
306
 
307
+ var backHandler = null;
308
+
309
+ function setBackActionFn(handler) {
310
+ backHandler = typeof handler === 'function' ? handler : null;
311
+ postEnvelope({ type: 'set_back_action', shown: backHandler !== null });
312
+ }
313
+
307
314
  function fireChannel(channel, payload) {
308
315
  var list = channelHandlers[channel];
309
316
  if (!list || list.length === 0) {
@@ -348,6 +355,12 @@ export function buildUiShim(options: UiShimOptions): string {
348
355
  }
349
356
  return;
350
357
  }
358
+ if (type === 'back') {
359
+ if (backHandler) {
360
+ try { backHandler(); } catch (e) {}
361
+ }
362
+ return;
363
+ }
351
364
  if (type === 'cancel' && typeof envelope.requestId === 'string') {
352
365
  // Background-side cancel (reserved; currently unused). If we had a
353
366
  // future bidirectional RPC this would abort an in-flight UI handler.
@@ -366,6 +379,7 @@ export function buildUiShim(options: UiShimOptions): string {
366
379
  onClose: onClose,
367
380
  ready: readyFn,
368
381
  setBackgroundColor: setBackgroundColorFn,
382
+ setBackAction: setBackActionFn,
369
383
  /** @internal — packageName for diagnostic logs only. */
370
384
  _packageName: packageName,
371
385
  };
@@ -132,13 +132,6 @@ export const SETTINGS: Record<string, Setting> = {
132
132
  saveOnServer: true,
133
133
  persist: true,
134
134
  },
135
- memomind_accessory_picker: {
136
- key: "memomind_accessory_picker",
137
- defaultValue: () => true,
138
- writable: true,
139
- saveOnServer: false,
140
- persist: true,
141
- },
142
135
  debug_console: {
143
136
  key: "debug_console",
144
137
  defaultValue: () => false,
@@ -950,6 +943,13 @@ export const SETTINGS: Record<string, Setting> = {
950
943
  saveOnServer: false,
951
944
  persist: true,
952
945
  },
946
+ debug_mirror_rail: {
947
+ key: "debug_mirror_rail",
948
+ defaultValue: () => false,
949
+ writable: true,
950
+ saveOnServer: false,
951
+ persist: true,
952
+ },
953
953
  home_offline_card: {
954
954
  key: "home_offline_card",
955
955
  defaultValue: () => true,
@@ -1274,7 +1274,6 @@ export const BLUETOOTH_SETTING_KEYS: string[] = [
1274
1274
  SETTINGS.cfw_scene_renderer.key,
1275
1275
  SETTINGS.cfw_fast_ble.key,
1276
1276
  SETTINGS.perf_logs.key,
1277
- SETTINGS.memomind_accessory_picker.key,
1278
1277
  SETTINGS.ring_via_glasses.key,
1279
1278
  SETTINGS.display_max_fps.key,
1280
1279
  SETTINGS.twelve_hour_time.key,
@@ -95,4 +95,7 @@ export const nimo: Capabilities = {
95
95
 
96
96
  // OTA capabilities - Nimo firmware updates are not driven by the ASG OTA flow
97
97
  hasOta: false,
98
+
99
+ // The drivers flash a bundled Glassly CFW over the GATT OTA characteristics.
100
+ hasCustomFirmware: true,
98
101
  }