@glassly/engine 0.3.0-beta.489 → 0.3.0-dev.462

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 (87) hide show
  1. package/build/engine.d.ts +0 -3
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/miniapps.d.ts +0 -6
  4. package/build/facades/miniapps.d.ts.map +1 -1
  5. package/build/facades/miniapps.js +0 -7
  6. package/build/facades/miniapps.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 +2 -2
  10. package/build/index.d.ts.map +1 -1
  11. package/build/index.js +2 -2
  12. package/build/index.js.map +1 -1
  13. package/build/services/AppRegistry.js +1 -1
  14. package/build/services/AppRegistry.js.map +1 -1
  15. package/build/services/LocalDisplayManager.d.ts +2 -1
  16. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  17. package/build/services/LocalDisplayManager.js +2 -1
  18. package/build/services/LocalDisplayManager.js.map +1 -1
  19. package/build/services/LocalMiniappRuntime.d.ts +9 -22
  20. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  21. package/build/services/LocalMiniappRuntime.js +52 -117
  22. package/build/services/LocalMiniappRuntime.js.map +1 -1
  23. package/build/services/MiniappLauncher.d.ts +0 -2
  24. package/build/services/MiniappLauncher.d.ts.map +1 -1
  25. package/build/services/MiniappLauncher.js +1 -6
  26. package/build/services/MiniappLauncher.js.map +1 -1
  27. package/build/services/pages/OverlayPages.d.ts +2 -13
  28. package/build/services/pages/OverlayPages.d.ts.map +1 -1
  29. package/build/services/pages/OverlayPages.js +10 -11
  30. package/build/services/pages/OverlayPages.js.map +1 -1
  31. package/build/services/pages/PageHost.d.ts +1 -1
  32. package/build/services/pages/PageHost.d.ts.map +1 -1
  33. package/build/services/pages/PageHost.js +2 -2
  34. package/build/services/pages/PageHost.js.map +1 -1
  35. package/build/services/pages/overlayApps.d.ts +13 -20
  36. package/build/services/pages/overlayApps.d.ts.map +1 -1
  37. package/build/services/pages/overlayApps.js +31 -50
  38. package/build/services/pages/overlayApps.js.map +1 -1
  39. package/build/services/settingsBridge.d.ts +14 -15
  40. package/build/services/settingsBridge.d.ts.map +1 -1
  41. package/build/services/settingsBridge.js +20 -20
  42. package/build/services/settingsBridge.js.map +1 -1
  43. package/build/stores/appVisibility.d.ts +0 -5
  44. package/build/stores/appVisibility.d.ts.map +1 -1
  45. package/build/stores/appVisibility.js +0 -2
  46. package/build/stores/appVisibility.js.map +1 -1
  47. package/build/stores/apps.d.ts +0 -5
  48. package/build/stores/apps.d.ts.map +1 -1
  49. package/build/stores/apps.js +10 -40
  50. package/build/stores/apps.js.map +1 -1
  51. package/build/stores/settings.d.ts.map +1 -1
  52. package/build/stores/settings.js +45 -19
  53. package/build/stores/settings.js.map +1 -1
  54. package/build/types/applet.d.ts +2 -2
  55. package/build/types/applet.d.ts.map +1 -1
  56. package/build/types/applet.js.map +1 -1
  57. package/build/utils/display/scene/anchor.d.ts +2 -1
  58. package/build/utils/display/scene/anchor.d.ts.map +1 -1
  59. package/build/utils/display/scene/anchor.js +2 -1
  60. package/build/utils/display/scene/anchor.js.map +1 -1
  61. package/package.json +7 -7
  62. package/src/facades/miniapps.ts +0 -9
  63. package/src/generated/releaseMetadata.ts +3 -3
  64. package/src/index.ts +1 -3
  65. package/src/services/AppRegistry.ts +1 -1
  66. package/src/services/LocalDisplayManager.ts +2 -1
  67. package/src/services/LocalMiniappRuntime.ts +52 -136
  68. package/src/services/MiniappLauncher.ts +1 -6
  69. package/src/services/pages/OverlayPages.ts +10 -11
  70. package/src/services/pages/PageHost.ts +2 -2
  71. package/src/services/pages/overlayApps.ts +30 -51
  72. package/src/services/settingsBridge.ts +20 -20
  73. package/src/stores/appVisibility.ts +0 -4
  74. package/src/stores/apps.ts +12 -44
  75. package/src/stores/settings.ts +45 -19
  76. package/src/types/applet.ts +1 -3
  77. package/src/utils/display/scene/anchor.ts +2 -1
  78. package/build/services/miniappPermissions.d.ts +0 -16
  79. package/build/services/miniappPermissions.d.ts.map +0 -1
  80. package/build/services/miniappPermissions.js +0 -7
  81. package/build/services/miniappPermissions.js.map +0 -1
  82. package/build/services/pages/activeDashboard.d.ts +0 -7
  83. package/build/services/pages/activeDashboard.d.ts.map +0 -1
  84. package/build/services/pages/activeDashboard.js +0 -21
  85. package/build/services/pages/activeDashboard.js.map +0 -1
  86. package/src/services/miniappPermissions.ts +0 -19
  87. package/src/services/pages/activeDashboard.ts +0 -24
@@ -2,7 +2,8 @@
2
2
  * Anchor resolution — turn corner-anchored boxes into plain top-left boxes
3
3
  * against the device's real canvas. Runs in emitScene BEFORE the canPosition
4
4
  * branch so both processScene (clamping) and degradeScene (reading order)
5
- * only ever see concrete coordinates.
5
+ * only ever see concrete coordinates. Spec:
6
+ * notes/superpowers/specs/2026-08-10-anchored-render-boxes.md
6
7
  */
7
8
  import type { SceneElementInput } from "./types";
8
9
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../../../../src/utils/display/scene/anchor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAA0B,iBAAiB,EAAC,MAAM,SAAS,CAAA;AAcvE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,iBAAiB,EAAE,CAKrB"}
1
+ {"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../../../../src/utils/display/scene/anchor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAA0B,iBAAiB,EAAC,MAAM,SAAS,CAAA;AAcvE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,iBAAiB,EAAE,CAKrB"}
@@ -2,7 +2,8 @@
2
2
  * Anchor resolution — turn corner-anchored boxes into plain top-left boxes
3
3
  * against the device's real canvas. Runs in emitScene BEFORE the canPosition
4
4
  * branch so both processScene (clamping) and degradeScene (reading order)
5
- * only ever see concrete coordinates.
5
+ * only ever see concrete coordinates. Spec:
6
+ * notes/superpowers/specs/2026-08-10-anchored-render-boxes.md
6
7
  */
7
8
  function resolveBox(box, width, height) {
8
9
  const { anchor, x, y, w, h } = box;
@@ -1 +1 @@
1
- {"version":3,"file":"anchor.js","sourceRoot":"","sources":["../../../../src/utils/display/scene/anchor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,SAAS,UAAU,CAAC,GAAkB,EAAE,KAAa,EAAE,MAAc;IACnE,MAAM,EAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAC,GAAG,GAAG,CAAA;IAChC,MAAM,KAAK,GAAG,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,cAAc,CAAA;IACjE,MAAM,MAAM,GAAG,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK,cAAc,CAAA;IACpE,OAAO;QACL,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;QACD,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAmC,EACnC,KAAa,EACb,MAAc;IAEd,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACtB,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM;YAAE,OAAO,EAAE,CAAA;QACzE,OAAO,EAAC,GAAG,EAAE,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,EAAC,CAAA;IACxD,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["/**\n * Anchor resolution — turn corner-anchored boxes into plain top-left boxes\n * against the device's real canvas. Runs in emitScene BEFORE the canPosition\n * branch so both processScene (clamping) and degradeScene (reading order)\n * only ever see concrete coordinates.\n */\n\nimport type {SceneBox, SceneBoxInput, SceneElementInput} from \"./types\"\n\nfunction resolveBox(box: SceneBoxInput, width: number, height: number): SceneBox {\n const {anchor, x, y, w, h} = box\n const right = anchor === \"top-right\" || anchor === \"bottom-right\"\n const bottom = anchor === \"bottom-left\" || anchor === \"bottom-right\"\n return {\n x: right ? width - x - w : x,\n y: bottom ? height - y - h : y,\n w,\n h,\n }\n}\n\n/**\n * Resolve every anchored box in a scene to top-left coordinates on a W×H\n * canvas. Unanchored elements pass through unchanged (same reference — no\n * copy churn on the common path), as do malformed ones so processScene's\n * validation still sees and reports them. Non-finite insets survive the\n * subtraction as non-finite and are dropped by the same validation.\n */\nexport function resolveAnchors(\n input: readonly SceneElementInput[],\n width: number,\n height: number,\n): SceneElementInput[] {\n return input.map((el) => {\n if (!el || typeof el !== \"object\" || !el.box || !el.box.anchor) return el\n return {...el, box: resolveBox(el.box, width, height)}\n })\n}\n"]}
1
+ {"version":3,"file":"anchor.js","sourceRoot":"","sources":["../../../../src/utils/display/scene/anchor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,SAAS,UAAU,CAAC,GAAkB,EAAE,KAAa,EAAE,MAAc;IACnE,MAAM,EAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAC,GAAG,GAAG,CAAA;IAChC,MAAM,KAAK,GAAG,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,cAAc,CAAA;IACjE,MAAM,MAAM,GAAG,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK,cAAc,CAAA;IACpE,OAAO;QACL,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;QACD,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAmC,EACnC,KAAa,EACb,MAAc;IAEd,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACtB,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM;YAAE,OAAO,EAAE,CAAA;QACzE,OAAO,EAAC,GAAG,EAAE,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,EAAC,CAAA;IACxD,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["/**\n * Anchor resolution — turn corner-anchored boxes into plain top-left boxes\n * against the device's real canvas. Runs in emitScene BEFORE the canPosition\n * branch so both processScene (clamping) and degradeScene (reading order)\n * only ever see concrete coordinates. Spec:\n * notes/superpowers/specs/2026-08-10-anchored-render-boxes.md\n */\n\nimport type {SceneBox, SceneBoxInput, SceneElementInput} from \"./types\"\n\nfunction resolveBox(box: SceneBoxInput, width: number, height: number): SceneBox {\n const {anchor, x, y, w, h} = box\n const right = anchor === \"top-right\" || anchor === \"bottom-right\"\n const bottom = anchor === \"bottom-left\" || anchor === \"bottom-right\"\n return {\n x: right ? width - x - w : x,\n y: bottom ? height - y - h : y,\n w,\n h,\n }\n}\n\n/**\n * Resolve every anchored box in a scene to top-left coordinates on a W×H\n * canvas. Unanchored elements pass through unchanged (same reference — no\n * copy churn on the common path), as do malformed ones so processScene's\n * validation still sees and reports them. Non-finite insets survive the\n * subtraction as non-finite and are dropped by the same validation.\n */\nexport function resolveAnchors(\n input: readonly SceneElementInput[],\n width: number,\n height: number,\n): SceneElementInput[] {\n return input.map((el) => {\n if (!el || typeof el !== \"object\" || !el.box || !el.box.anchor) return el\n return {...el, box: resolveBox(el.box, width, height)}\n })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.0-beta.489",
3
+ "version": "0.3.0-dev.462",
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.0-beta.489",
101
- "@glassly/cloud-client": "0.3.0-beta.489",
102
- "@glassly/cloud-protocol": "0.3.0-beta.489",
103
- "@glassly/crust": "0.3.0-beta.489",
104
- "@glassly/miniapp": "0.3.0-beta.489",
100
+ "@glassly/bluetooth-sdk": "0.3.0-dev.462",
101
+ "@glassly/cloud-client": "0.3.0-dev.462",
102
+ "@glassly/cloud-protocol": "0.3.0-dev.462",
103
+ "@glassly/crust": "0.3.0-dev.462",
104
+ "@glassly/miniapp": "0.3.0-dev.462",
105
105
  "buffer": "^6.0.3",
106
106
  "events": "^3.3.0",
107
107
  "react-native-marked": "8.1.1",
108
108
  "semver": "^7.7.4",
109
109
  "typesafe-ts": "^1.7.1",
110
- "@glassly/llm-providers": "0.3.0-beta.489"
110
+ "@glassly/llm-providers": "0.3.0-dev.462"
111
111
  },
112
112
  "devDependencies": {
113
113
  "@glassly/llm-providers": "workspace:*",
@@ -10,7 +10,6 @@
10
10
  */
11
11
  import {useAppStatusStore} from "../stores/apps"
12
12
  import localMiniappRuntime from "../services/LocalMiniappRuntime"
13
- import {activeDashboardAmong} from "../services/pages/activeDashboard"
14
13
  import type {ClientApp} from "../types"
15
14
 
16
15
  type AppsState = ReturnType<typeof useAppStatusStore.getState>
@@ -62,12 +61,4 @@ export const miniapps = {
62
61
  useAppStatusStore.getState().setHiddenStatus(...args),
63
62
  /** Mark a miniapp Always On (starts at launch, stops only from the switcher pill), persisted for the next boot. */
64
63
  setAlwaysOn: (...args: Parameters<AppsState["setAlwaysOn"]>) => useAppStatusStore.getState().setAlwaysOn(...args),
65
-
66
- /** Installed miniapps that can hold the dashboard role (manifest type "dashboard"). */
67
- dashboards: (): ClientApp[] => useAppStatusStore.getState().apps.filter((app) => app.type === "dashboard"),
68
- /** The package that holds the dashboard role, or null when no dashboard is installed. */
69
- activeDashboard: (): string | null => activeDashboardAmong(useAppStatusStore.getState().apps),
70
- /** Give the dashboard role to an installed dashboard; the previous holder stops and the new one starts. */
71
- setActiveDashboard: (...args: Parameters<AppsState["setActiveDashboard"]>) =>
72
- useAppStatusStore.getState().setActiveDashboard(...args),
73
64
  }
@@ -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.0",
15
- "releaseIdentity": "0.3.0-beta.489",
16
- "releaseSetId": "glassly-0.3.0-beta.489",
17
- "sourceCommit": "fe72aed649a9cfa4186f114cf47766bffad566b6",
15
+ "releaseIdentity": "0.3.0-dev.462",
16
+ "releaseSetId": "glassly-0.3.0-dev.462",
17
+ "sourceCommit": "6d9eaccddb17582a14925742c5da672feeca3cee",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
package/src/index.ts CHANGED
@@ -23,7 +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 {DEFAULT_HIDDEN_PACKAGES, isGatedByDevTools, isSystemMiniapp} from "./stores/appVisibility"
26
+ export {DEFAULT_HIDDEN_PACKAGES, isGatedByDevTools} from "./stores/appVisibility"
27
27
  export type {WifiSearchResult} from "./facades/glassesWifi"
28
28
  export type {IdentitySnapshot} from "./services/PairingIdentity"
29
29
  export type {DisplayMirrorEvent} from "./facades/displayMirror"
@@ -182,8 +182,6 @@ export {
182
182
  useUninstall,
183
183
  useActiveApps,
184
184
  useRunningAlwaysOnApps,
185
- useDashboards,
186
- useActiveDashboard,
187
185
  useActiveBackgroundApps,
188
186
  useBackgroundApps,
189
187
  useActiveForegroundApp,
@@ -76,7 +76,7 @@ export function normalizeManifestPermissions(
76
76
  }
77
77
 
78
78
  function normalizeManifestType(raw: unknown): AppletType {
79
- return raw === "background" || raw === "dashboard" || raw === "standard" ? raw : "standard"
79
+ return raw === "background" || raw === "system_dashboard" || raw === "standard" ? raw : "standard"
80
80
  }
81
81
 
82
82
  /**
@@ -10,7 +10,8 @@
10
10
  * - core-app vs background-app arbitration with a background lock
11
11
  *
12
12
  * Scope is deliberately local-only. Cloud-originated displays are untouched
13
- * and may race with local displays during dev; that's acceptable.
13
+ * and may race with local displays during dev; that's acceptable per plan
14
+ * (notes/archive/agents/local-display-manager-plan.md).
14
15
  *
15
16
  * Display pacing is NOT done here. A JS-side ~300ms throttle used to live in
16
17
  * this class, but JS timers (and therefore the throttle's trailing flush) do
@@ -77,19 +77,7 @@ import {
77
77
  validateSettingsSet,
78
78
  type DerivedSettingsKey,
79
79
  } from "./settingsBridge"
80
- import {mayUsePermission} from "./miniappPermissions"
81
- import {activeDashboardAmong} from "./pages/activeDashboard"
82
- import {
83
- activeDashboard,
84
- addDashboardApp,
85
- displayViewsFor,
86
- isDashboardApp,
87
- isOverlayApp,
88
- onOverlayAppsChange,
89
- removeDashboardApp,
90
- setActiveDashboard,
91
- setOverlayViewer,
92
- } from "./pages/overlayApps"
80
+ import {addOverlayApp, displayViewsFor, isOverlayApp, removeOverlayApp, setOverlayViewer} from "./pages/overlayApps"
93
81
  import {pagesEligible} from "./pages/pageEligibility"
94
82
  import nativeNavigationService from "./NativeNavigationService"
95
83
  import type {DisplayPayload} from "./LocalDisplayManager"
@@ -132,7 +120,6 @@ import ttsModelManager from "./TTSModelManager"
132
120
  import {NavigationHandlers} from "./NavigationHandlers"
133
121
  import type {ClientApp} from "../types/applet"
134
122
  import {useAppStatusStore} from "../stores/apps"
135
- import {isSystemMiniapp} from "../stores/appVisibility"
136
123
  import {getDevAppAttestation, getDevAppSourcePackage} from "./AppRegistry"
137
124
  import {resolveForegroundLocationPermission} from "./ForegroundLocationPermission"
138
125
  import {advanceMiniappPingLiveness} from "./MiniappLiveness"
@@ -266,6 +253,9 @@ const SYSTEM_MINIAPP_PACKAGES = new Set([
266
253
  "com.glassly.ai",
267
254
  "com.glassly.feedback",
268
255
  "com.glassly.miniappdev",
256
+ // The dashboard: the settings bridge and the miniapp list/start/stop are its side menu and settings
257
+ // page, and the notification app list is its notification settings' per-app filter.
258
+ "com.glassly.dashboard",
269
259
  ])
270
260
  const PING_INTERVAL_MS = 5_000
271
261
  // Min gap between "transcription limit reached" notices on the glasses. A
@@ -352,8 +342,6 @@ const PERMISSION_TYPE_TO_CANONICAL: Record<string, string> = {
352
342
  MEMORY: "memory",
353
343
  AI: "ai",
354
344
  SPEAKERS: "speakers",
355
- MANAGE_MINIAPPS: "manageMiniapps",
356
- PLATFORM_SETTINGS: "platformSettings",
357
345
  }
358
346
 
359
347
  /**
@@ -468,8 +456,6 @@ const ALL_CANONICAL_PERMISSIONS = [
468
456
  "ai",
469
457
  "speakers",
470
458
  "displayMirror",
471
- "manageMiniapps",
472
- "platformSettings",
473
459
  ] as const
474
460
 
475
461
  /**
@@ -1177,11 +1163,11 @@ class LocalMiniappRuntime {
1177
1163
  // Drop this app's transcription hints; the cloud subscription rebuild
1178
1164
  // below (via replaceStreamSubscribers) re-derives the hint union without it.
1179
1165
  this.transcriptionHintsByApp.delete(packageName)
1180
- // A dashboard is going: release the slot if it holds it, and drop the
1166
+ // An overlay app is going: release the slot if it holds it, and drop the
1181
1167
  // dashboard view from its capabilities record.
1182
- if (isDashboardApp(packageName)) {
1168
+ if (isOverlayApp(packageName)) {
1183
1169
  overlayPages.remove(packageName)
1184
- removeDashboardApp(packageName)
1170
+ removeOverlayApp(packageName)
1185
1171
  }
1186
1172
  // Its gesture claims go back to whoever claimed them before.
1187
1173
  this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName))
@@ -1565,11 +1551,7 @@ class LocalMiniappRuntime {
1565
1551
  this.handleOpenUrl(packageName, payload)
1566
1552
  break
1567
1553
  case MiniappRequestType.DISPLAY_DIAGNOSTICS: {
1568
- // The records are parsed out of the G2PERF lines, which the SDK only logs while g2_perf_logs is on.
1569
- const settings = useSettingsStore.getState()
1570
- const enabled =
1571
- settings.getSetting(SETTINGS.debug_console.key) === true &&
1572
- settings.getSetting(SETTINGS.g2_perf_logs.key) === true
1554
+ const enabled = useSettingsStore.getState().getSetting(SETTINGS.debug_console.key) === true
1573
1555
  this.sendResult(packageName, requestId, true, {
1574
1556
  enabled,
1575
1557
  generatedAt: Date.now(),
@@ -1766,12 +1748,9 @@ class LocalMiniappRuntime {
1766
1748
  existing.lastPongAt = Date.now()
1767
1749
  existing.unansweredPingRounds = 0
1768
1750
 
1769
- // The dashboard that holds the role may take the overlay slot: its
1770
- // capabilities carry the dashboard view from the start.
1771
- if (existing.installedManifest?.type === "dashboard") {
1772
- addDashboardApp(packageName)
1773
- this.syncActiveDashboard()
1774
- }
1751
+ // A system_dashboard app may take the overlay slot: its capabilities
1752
+ // carry the dashboard view from the start.
1753
+ if (existing.installedManifest?.type === "system_dashboard") addOverlayApp(packageName)
1775
1754
  const capabilities = this.capabilitiesFor(packageName)
1776
1755
 
1777
1756
  // Build the declared-permission record for the SDK's session.permissions
@@ -2142,26 +2121,24 @@ class LocalMiniappRuntime {
2142
2121
  const required = permissionForStream(stream)
2143
2122
  if (required && !declaredTypes.has(required)) {
2144
2123
  logPermissionNotDeclared(packageName, required, `to subscribe to "${stream}"`, `{"type": "${required}"}`)
2145
- const error = {
2124
+ this.sendResult(packageName, requestId, false, undefined, {
2146
2125
  code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
2147
2126
  message: `${required} permission not declared in miniapp.json (required for "${stream}"). Add {"type": "${required}"} to the "permissions" array.`,
2148
2127
  // Extra context fields read by the SDK so authors that subscribe
2149
2128
  // to session.on("error") can format their own messages.
2150
2129
  permission: required,
2151
2130
  subscription: stream,
2152
- }
2153
- // The SDK sends SUBSCRIBE without a requestId, so push the rejection.
2154
- if (requestId) this.sendResult(packageName, requestId, false, undefined, error)
2155
- else this.sendToMiniapp(packageName, {type: MiniappResponseType.ERROR, ...error})
2131
+ })
2156
2132
  return
2157
2133
  }
2158
2134
  }
2159
2135
 
2160
- // The settings stream carries the wearer's settings.
2161
- if (
2162
- streams.includes(MiniappStreamType.SETTINGS_CHANGED) &&
2163
- !this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to subscribe to the settings stream")
2164
- ) {
2136
+ // The settings stream carries the wearer's settings: system apps only.
2137
+ if (streams.includes(MiniappStreamType.SETTINGS_CHANGED) && !this.isSystemPackage(packageName)) {
2138
+ this.sendResult(packageName, requestId, false, undefined, {
2139
+ code: MiniappErrorCode.NOT_PERMITTED,
2140
+ message: "The settings stream is restricted to system apps",
2141
+ })
2165
2142
  return
2166
2143
  }
2167
2144
 
@@ -2175,9 +2152,8 @@ class LocalMiniappRuntime {
2175
2152
  [...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
2176
2153
  )
2177
2154
  this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
2178
- // The overlay app presents phone notifications on the glasses while it
2179
- // listens to them; recorded for every dashboard so the role can move.
2180
- if (isDashboardApp(packageName)) {
2155
+ // An overlay app that listens to phone notifications presents them on the glasses.
2156
+ if (isOverlayApp(packageName)) {
2181
2157
  setOverlayViewer(packageName, app.subscriptions.has(MiniappStreamType.PHONE_NOTIFICATION))
2182
2158
  }
2183
2159
 
@@ -2409,13 +2385,13 @@ class LocalMiniappRuntime {
2409
2385
  })
2410
2386
  return
2411
2387
  }
2412
- // The dashboard view is the overlay app's alone; anyone else's frame
2388
+ // The dashboard view is the overlay apps' alone; anyone else's frame
2413
2389
  // is dropped, reported rather than silently lost.
2414
2390
  if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
2415
2391
  this.sendResult(packageName, requestId, true, {
2416
2392
  status: "displayed",
2417
2393
  presented: false,
2418
- reason: "not the active dashboard",
2394
+ reason: "not the dashboard app",
2419
2395
  })
2420
2396
  return
2421
2397
  }
@@ -2439,11 +2415,10 @@ class LocalMiniappRuntime {
2439
2415
  }
2440
2416
 
2441
2417
  /**
2442
- * display.showView: the active dashboard takes or releases the overlay
2443
- * slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
2444
- * "unsupported" for a dashboard without the role and while the glasses
2445
- * cannot show the dashboard view. Idempotent either way; the slot's state
2446
- * comes back to the app as a display_view change.
2418
+ * display.showView: the designated dashboard app takes or releases the
2419
+ * overlay slot. Refused (NOT_PERMITTED) for any other app; "unsupported"
2420
+ * while the glasses cannot show the dashboard view. Idempotent either way;
2421
+ * the slot's state comes back to the app as a display_view change.
2447
2422
  */
2448
2423
  private handleShowView(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2449
2424
  if (!this.connectedApps.has(packageName)) {
@@ -2458,17 +2433,13 @@ class LocalMiniappRuntime {
2458
2433
  })
2459
2434
  return
2460
2435
  }
2461
- if (!isDashboardApp(packageName)) {
2436
+ if (!isOverlayApp(packageName)) {
2462
2437
  this.sendResult(packageName, requestId, false, undefined, {
2463
2438
  code: MiniappErrorCode.NOT_PERMITTED,
2464
- message: 'The dashboard view belongs to miniapps of type "dashboard"',
2439
+ message: "The dashboard view belongs to system_dashboard apps",
2465
2440
  })
2466
2441
  return
2467
2442
  }
2468
- if (!isOverlayApp(packageName)) {
2469
- this.sendResult(packageName, requestId, true, {status: "unsupported", reason: "not the active dashboard"})
2470
- return
2471
- }
2472
2443
  if (view === "main") {
2473
2444
  overlayPages.hide(packageName)
2474
2445
  this.sendResult(packageName, requestId, true, {status: "shown"})
@@ -2492,36 +2463,17 @@ class LocalMiniappRuntime {
2492
2463
  )
2493
2464
  }
2494
2465
 
2495
- /**
2496
- * Reject the request with PERMISSION_NOT_DECLARED unless the caller's
2497
- * manifest declares `permission`. System apps pass without declaring it.
2498
- */
2499
- private requirePermission(
2500
- packageName: string,
2501
- requestId: string | undefined,
2502
- permission: string,
2503
- context: string,
2504
- ): boolean {
2505
- const caller = {
2506
- system: this.isSystemPackage(packageName),
2507
- permissions: this.connectedApps.get(packageName)?.installedManifest?.permissions,
2508
- }
2509
- if (mayUsePermission(caller, permission)) return true
2510
- logPermissionNotDeclared(packageName, permission, context, `{"type": "${permission}"}`)
2511
- this.sendResult(packageName, requestId, false, undefined, {
2512
- code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
2513
- message: `${permission} permission not declared in miniapp.json. Add {"type": "${permission}"} to the "permissions" array.`,
2514
- permission,
2515
- })
2516
- return false
2517
- }
2518
-
2519
2466
  // ---------------------------------------------------------------------------
2520
- // Settings bridge (session.settings) — PLATFORM_SETTINGS permission
2467
+ // Settings bridge (session.settings) — system apps only
2521
2468
  // ---------------------------------------------------------------------------
2522
2469
 
2523
- private requireSettingsPermission(packageName: string, requestId: string | undefined): boolean {
2524
- return this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to use the settings bridge")
2470
+ private requireSystemForSettings(packageName: string, requestId: string | undefined): boolean {
2471
+ if (this.isSystemPackage(packageName)) return true
2472
+ this.sendResult(packageName, requestId, false, undefined, {
2473
+ code: MiniappErrorCode.NOT_PERMITTED,
2474
+ message: "The settings bridge is restricted to system apps",
2475
+ })
2476
+ return false
2525
2477
  }
2526
2478
 
2527
2479
  /** A bridged key's current value: the store's, or the engine's for a derived key. */
@@ -2538,13 +2490,11 @@ class LocalMiniappRuntime {
2538
2490
  return localDisplayManager.foregroundAppOnScreen()
2539
2491
  case "ring_battery":
2540
2492
  return ringBatteryFrom(useGlassesStore.getState())
2541
- case "active_dashboard":
2542
- return activeDashboard()
2543
2493
  }
2544
2494
  }
2545
2495
 
2546
2496
  private handleSettingsGet(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2547
- if (!this.requireSettingsPermission(packageName, requestId)) return
2497
+ if (!this.requireSystemForSettings(packageName, requestId)) return
2548
2498
  const values: Record<string, unknown> = {}
2549
2499
  for (const key of selectSettingsBridgeKeys(payload.keys)) values[key] = this.settingsBridgeValue(key)
2550
2500
  this.sendResult(packageName, requestId, true, {values})
@@ -2555,7 +2505,7 @@ class LocalMiniappRuntime {
2555
2505
  payload: Record<string, unknown>,
2556
2506
  requestId?: string,
2557
2507
  ): Promise<void> {
2558
- if (!this.requireSettingsPermission(packageName, requestId)) return
2508
+ if (!this.requireSystemForSettings(packageName, requestId)) return
2559
2509
  const validated = validateSettingsSet(payload)
2560
2510
  if (!validated.ok) {
2561
2511
  this.sendResult(packageName, requestId, false, undefined, {code: validated.code, message: validated.message})
@@ -2575,11 +2525,7 @@ class LocalMiniappRuntime {
2575
2525
  }
2576
2526
 
2577
2527
  private async handleNotificationAppsList(packageName: string, requestId?: string): Promise<void> {
2578
- if (
2579
- !this.requirePermission(packageName, requestId, "READ_NOTIFICATIONS", "to list the apps that post notifications")
2580
- ) {
2581
- return
2582
- }
2528
+ if (!this.requireSystemForSettings(packageName, requestId)) return
2583
2529
  try {
2584
2530
  this.sendResult(packageName, requestId, true, await installedNotificationApps())
2585
2531
  } catch (err) {
@@ -2593,9 +2539,8 @@ class LocalMiniappRuntime {
2593
2539
  /**
2594
2540
  * Push bridged settings as they change: stored keys from the store, the
2595
2541
  * derived keys whenever their inputs move (phone activity, the display,
2596
- * the apps store, the ring, the dashboard role). Subscribers declared
2597
- * PLATFORM_SETTINGS (gated at SUBSCRIBE), so this is a no-op fan-out for
2598
- * everyone else.
2542
+ * the apps store, the ring). Subscribers are system apps only (gated at
2543
+ * SUBSCRIBE), so this is a no-op fan-out for everyone else.
2599
2544
  */
2600
2545
  private ensureSettingsBridgeWired(): void {
2601
2546
  if (this.settingsBridgeWired) return
@@ -2631,7 +2576,6 @@ class LocalMiniappRuntime {
2631
2576
  onAppForegroundChange((foregrounded) => this.broadcastVisibility(foregrounded))
2632
2577
  localDisplayManager.onActiveDisplayChange(pushDerived)
2633
2578
  useAppStatusStore.subscribe(pushDerived)
2634
- onOverlayAppsChange(pushDerived)
2635
2579
  useGlassesStore.subscribe(
2636
2580
  (s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.controllerConnected}:${s.controllerBatteryLevel}`,
2637
2581
  pushDerived,
@@ -5252,31 +5196,6 @@ class LocalMiniappRuntime {
5252
5196
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast)
5253
5197
  // The dashboard master switch flips the dashboard view.
5254
5198
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.custom_dashboard_enabled.key), broadcast)
5255
- // The wearer's choice and the installed dashboards decide who holds the role.
5256
- const syncRole = () => this.syncActiveDashboard()
5257
- useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.active_dashboard.key), syncRole)
5258
- useAppStatusStore.subscribe(syncRole)
5259
- syncRole()
5260
- }
5261
-
5262
- /**
5263
- * Recompute who holds the dashboard role (ADR-0009). The previous holder
5264
- * gives up the slot first, so it hears the view change while it is still
5265
- * the overlay app; then every app sees its views again.
5266
- */
5267
- private syncActiveDashboard(): void {
5268
- const candidates: Array<{packageName: string; type?: string}> = [...useAppStatusStore.getState().apps]
5269
- // A dashboard can connect at launch before the apps store has listed it.
5270
- for (const [packageName, app] of this.connectedApps) {
5271
- if (candidates.some((candidate) => candidate.packageName === packageName)) continue
5272
- candidates.push({packageName, type: app.installedManifest?.type})
5273
- }
5274
- const next = activeDashboardAmong(candidates)
5275
- const previous = activeDashboard()
5276
- if (next === previous) return
5277
- if (previous !== null) overlayPages.hide(previous, "replaced")
5278
- setActiveDashboard(next)
5279
- this.broadcastCapabilities()
5280
5199
  }
5281
5200
 
5282
5201
  /** Capabilities as `packageName` sees them: the model profile plus the views this app may draw. */
@@ -5749,20 +5668,18 @@ class LocalMiniappRuntime {
5749
5668
  }
5750
5669
  }
5751
5670
 
5752
- /** Reject the request unless the caller may list, start and stop other miniapps (MANAGE_MINIAPPS). */
5753
- private requireMiniappsPermission(
5671
+ /** Reject the request with NOT_PERMITTED unless the caller is a system app. */
5672
+ private requireSystemCaller(
5754
5673
  packageName: string,
5755
5674
  requestId: string | undefined,
5756
5675
  op: InteropAuditEvent["op"],
5757
5676
  target?: string,
5758
5677
  ): boolean {
5759
- if (this.requirePermission(packageName, requestId, "MANAGE_MINIAPPS", `to ${op} miniapps`)) return true
5760
- this.auditInterop({
5761
- caller: packageName,
5762
- op,
5763
- target,
5764
- ok: false,
5765
- errorCode: MiniappErrorCode.PERMISSION_NOT_DECLARED,
5678
+ if (this.isSystemPackage(packageName)) return true
5679
+ this.auditInterop({caller: packageName, op, target, ok: false, errorCode: MiniappErrorCode.NOT_PERMITTED})
5680
+ this.sendResult(packageName, requestId, false, undefined, {
5681
+ code: MiniappErrorCode.NOT_PERMITTED,
5682
+ message: "Inter-miniapp APIs are restricted to system apps",
5766
5683
  })
5767
5684
  return false
5768
5685
  }
@@ -5781,14 +5698,13 @@ class LocalMiniappRuntime {
5781
5698
  version: app.version ?? "",
5782
5699
  running: app.running,
5783
5700
  hidden: app.hidden === true,
5784
- system: isSystemMiniapp(app),
5785
5701
  compatibility,
5786
5702
  actions: app.actions ?? [],
5787
5703
  }
5788
5704
  }
5789
5705
 
5790
5706
  private handleMiniappsList(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
5791
- if (!this.requireMiniappsPermission(packageName, requestId, "list")) return
5707
+ if (!this.requireSystemCaller(packageName, requestId, "list")) return
5792
5708
  const includeIncompatible = payload.includeIncompatible === true
5793
5709
  const infos = this.interopApps()
5794
5710
  .filter(
@@ -5809,7 +5725,7 @@ class LocalMiniappRuntime {
5809
5725
  requestId?: string,
5810
5726
  ): Promise<void> {
5811
5727
  const target = payload.packageName as string | undefined
5812
- if (!this.requireMiniappsPermission(packageName, requestId, "start", target)) return
5728
+ if (!this.requireSystemCaller(packageName, requestId, "start", target)) return
5813
5729
  const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined
5814
5730
  if (!target || !app) {
5815
5731
  this.sendResult(packageName, requestId, false, undefined, {
@@ -5901,7 +5817,7 @@ class LocalMiniappRuntime {
5901
5817
  requestId?: string,
5902
5818
  ): Promise<void> {
5903
5819
  const target = payload.packageName as string | undefined
5904
- if (!this.requireMiniappsPermission(packageName, requestId, "stop", target)) return
5820
+ if (!this.requireSystemCaller(packageName, requestId, "stop", target)) return
5905
5821
  if (!target) {
5906
5822
  this.sendResult(packageName, requestId, false, undefined, {
5907
5823
  code: MiniappErrorCode.APP_NOT_FOUND,
@@ -31,7 +31,6 @@ import {storage} from "../utils/storage/storage"
31
31
  import appRegistry, {getLocalAppRunningState, saveLocalAppRunningState} from "./AppRegistry"
32
32
  import devServerBridge from "./DevServerBridge"
33
33
  import localMiniappRuntime, {type InstalledMiniappManifest} from "./LocalMiniappRuntime"
34
- import {activeDashboardAmong} from "./pages/activeDashboard"
35
34
  import type {MiniappLaunchSource} from "@glassly/miniapp"
36
35
  import type {JSRouter} from "./JSRouter"
37
36
 
@@ -307,21 +306,17 @@ class MiniappLauncher {
307
306
  *
308
307
  * Always On apps (the dashboard) start whether or not they were running
309
308
  * when the app was last killed, and get a switcher card like any other app.
310
- * A dashboard that does not hold the role (ADR-0009) is not started by its
311
- * Always On flag.
312
309
  * Both restore paths skip apps missing required OS permissions without
313
310
  * prompting or clearing their saved running/Always On choice.
314
311
  */
315
312
  async autostartLocalMiniapps(): Promise<void> {
316
313
  const apps = await appRegistry.getInstalledMiniapps()
317
- const activeDashboard = activeDashboardAmong(apps)
318
314
  for (const app of apps) {
319
315
  // Only phone-hosted miniapps have a JSContext to re-spawn. Offline
320
316
  // built-ins (`local:false, offline:true`) restore their native running
321
317
  // state elsewhere and have no bundle to launch.
322
318
  if (!app.local) continue
323
- const alwaysOn = app.alwaysOn && (app.type !== "dashboard" || app.packageName === activeDashboard)
324
- if (!alwaysOn && !getLocalAppRunningState(app.packageName)) continue
319
+ if (!app.alwaysOn && !getLocalAppRunningState(app.packageName)) continue
325
320
  if (this.isRunning(app.packageName)) continue
326
321
  try {
327
322
  const missing = await permissions.missingRequired(app.permissions ?? [])
@@ -1,12 +1,11 @@
1
1
  /**
2
- * The overlay app's page (ADR-0006, ADR-0009): one Page per dashboard that
3
- * has taken the slot, the only Page implementation the page host shows. The
4
- * engine keeps the slot and the eligibility watch; a page only relays:
5
- * holding the slot is reported to its app as a view change, and the app's
6
- * frames reach the slot through `renderOverlay` under its package name. Only
7
- * the dashboard that holds the role may show; when the role moves, the
8
- * previous holder hears it was replaced. The runtime supplies the sink that
9
- * delivers to apps, so this module never imports it.
2
+ * The overlay apps' pages (ADR-0006): one Page per app that has taken the
3
+ * slot, the only Page implementation the page host shows. The engine keeps
4
+ * the slot and the eligibility watch; a page only relays: holding the slot
5
+ * is reported to its app as a view change, and the app's frames reach the
6
+ * slot through `renderOverlay` under its package name. Whichever app asked
7
+ * last holds the slot; the previous one hears it was replaced. The runtime
8
+ * supplies the sink that delivers to apps, so this module never imports it.
10
9
  */
11
10
  import pageHost, {type Page, type PageHiddenReason} from "./PageHost"
12
11
  import {isOverlayApp} from "./overlayApps"
@@ -45,10 +44,10 @@ class OverlayPages {
45
44
  return pageHost.showing === page
46
45
  }
47
46
 
48
- /** Release the slot if the app holds it; "replaced" when the dashboard role moved on. */
49
- hide(packageName: string, reason: PageHiddenReason = "hidden"): void {
47
+ /** Release the slot if the app holds it. */
48
+ hide(packageName: string): void {
50
49
  const page = this.pages.get(packageName)
51
- if (page) pageHost.hide(page, reason)
50
+ if (page) pageHost.hide(page)
52
51
  }
53
52
 
54
53
  /** The app stopped: release the slot without telling it, and forget its page. */
@@ -72,10 +72,10 @@ class PageHost {
72
72
  }
73
73
 
74
74
  /** Release the slot if `page` holds it. */
75
- hide(page: Page, reason: PageHiddenReason = "hidden"): void {
75
+ hide(page: Page): void {
76
76
  this.enqueue(() => {
77
77
  if (this.showingPage !== page) return
78
- this.release(reason)
78
+ this.release("hidden")
79
79
  })
80
80
  }
81
81