@glassly/engine 0.3.0-dev.460 → 0.3.0-dev.465

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 (75) hide show
  1. package/build/engine.d.ts +3 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/miniapps.d.ts +6 -0
  4. package/build/facades/miniapps.d.ts.map +1 -1
  5. package/build/facades/miniapps.js +7 -0
  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 +1 -1
  10. package/build/index.d.ts.map +1 -1
  11. package/build/index.js +1 -1
  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/BlobStore.js +2 -2
  16. package/build/services/BlobStore.js.map +1 -1
  17. package/build/services/LocalMiniappRuntime.d.ts +22 -9
  18. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  19. package/build/services/LocalMiniappRuntime.js +104 -49
  20. package/build/services/LocalMiniappRuntime.js.map +1 -1
  21. package/build/services/MiniappLauncher.d.ts +2 -0
  22. package/build/services/MiniappLauncher.d.ts.map +1 -1
  23. package/build/services/MiniappLauncher.js +6 -1
  24. package/build/services/MiniappLauncher.js.map +1 -1
  25. package/build/services/miniappPermissions.d.ts +16 -0
  26. package/build/services/miniappPermissions.d.ts.map +1 -0
  27. package/build/services/miniappPermissions.js +7 -0
  28. package/build/services/miniappPermissions.js.map +1 -0
  29. package/build/services/pages/OverlayPages.d.ts +13 -2
  30. package/build/services/pages/OverlayPages.d.ts.map +1 -1
  31. package/build/services/pages/OverlayPages.js +11 -10
  32. package/build/services/pages/OverlayPages.js.map +1 -1
  33. package/build/services/pages/PageHost.d.ts +1 -1
  34. package/build/services/pages/PageHost.d.ts.map +1 -1
  35. package/build/services/pages/PageHost.js +2 -2
  36. package/build/services/pages/PageHost.js.map +1 -1
  37. package/build/services/pages/activeDashboard.d.ts +7 -0
  38. package/build/services/pages/activeDashboard.d.ts.map +1 -0
  39. package/build/services/pages/activeDashboard.js +21 -0
  40. package/build/services/pages/activeDashboard.js.map +1 -0
  41. package/build/services/pages/overlayApps.d.ts +20 -13
  42. package/build/services/pages/overlayApps.d.ts.map +1 -1
  43. package/build/services/pages/overlayApps.js +50 -31
  44. package/build/services/pages/overlayApps.js.map +1 -1
  45. package/build/services/settingsBridge.d.ts +15 -14
  46. package/build/services/settingsBridge.d.ts.map +1 -1
  47. package/build/services/settingsBridge.js +20 -20
  48. package/build/services/settingsBridge.js.map +1 -1
  49. package/build/stores/apps.d.ts +5 -0
  50. package/build/stores/apps.d.ts.map +1 -1
  51. package/build/stores/apps.js +40 -2
  52. package/build/stores/apps.js.map +1 -1
  53. package/build/stores/settings.d.ts.map +1 -1
  54. package/build/stores/settings.js +6 -45
  55. package/build/stores/settings.js.map +1 -1
  56. package/build/types/applet.d.ts +2 -2
  57. package/build/types/applet.d.ts.map +1 -1
  58. package/build/types/applet.js.map +1 -1
  59. package/package.json +7 -7
  60. package/src/facades/miniapps.ts +9 -0
  61. package/src/generated/releaseMetadata.ts +3 -3
  62. package/src/index.ts +2 -0
  63. package/src/services/AppRegistry.ts +1 -1
  64. package/src/services/BlobStore.ts +2 -2
  65. package/src/services/LocalMiniappRuntime.ts +124 -49
  66. package/src/services/MiniappLauncher.ts +6 -1
  67. package/src/services/miniappPermissions.ts +19 -0
  68. package/src/services/pages/OverlayPages.ts +11 -10
  69. package/src/services/pages/PageHost.ts +2 -2
  70. package/src/services/pages/activeDashboard.ts +24 -0
  71. package/src/services/pages/overlayApps.ts +51 -30
  72. package/src/services/settingsBridge.ts +20 -20
  73. package/src/stores/apps.ts +44 -2
  74. package/src/stores/settings.ts +6 -45
  75. package/src/types/applet.ts +3 -1
@@ -1,51 +1,71 @@
1
1
  /**
2
- * The overlay apps: the connected miniapps whose manifest type is
3
- * "system_dashboard" (the dashboard). They alone may take the overlay slot
4
- * through `session.display.showView("dashboard")` and have "dashboard" in
5
- * their `display.views` capability while the glasses are eligible. The runtime adds an app when it connects and
6
- * removes it when it stops; the engine never knows what any of them draws.
7
- * Kept free of imports so the runtime and the notification policy can both
8
- * read it without a cycle.
2
+ * The overlay app: the connected miniapp that holds the dashboard role
3
+ * (ADR-0009). It alone may take the overlay slot through
4
+ * `session.display.showView("dashboard")` and has "dashboard" in its
5
+ * `display.views` capability while the glasses are eligible. The runtime
6
+ * records each dashboard miniapp (manifest type "dashboard") as it connects
7
+ * and stops, and who holds the role; the engine never knows what a dashboard
8
+ * draws. Kept free of imports so the runtime and the notification policy can
9
+ * both read it without a cycle.
9
10
  */
10
11
 
11
12
  export type DisplayView = "main" | "dashboard"
12
13
 
13
- const apps = new Set<string>()
14
- /** Overlay apps listening to the phone's notifications: something on the glasses presents them. */
15
- const viewers = new Set<string>()
14
+ /** Connected dashboard miniapps, role or not. */
15
+ const connected = new Set<string>()
16
+ /** Connected dashboards listening to the phone's notifications. */
17
+ const listening = new Set<string>()
18
+ let active: string | null = null
16
19
  const listeners = new Set<() => void>()
17
20
 
21
+ /** `packageName` holds the dashboard role and is connected. */
18
22
  export function isOverlayApp(packageName: string): boolean {
19
- return apps.has(packageName)
23
+ return packageName === active && connected.has(packageName)
24
+ }
25
+
26
+ /** `packageName` is a connected dashboard miniapp, whether or not it holds the role. */
27
+ export function isDashboardApp(packageName: string): boolean {
28
+ return connected.has(packageName)
20
29
  }
21
30
 
22
31
  export function overlayApps(): readonly string[] {
23
- return [...apps]
32
+ return active !== null && connected.has(active) ? [active] : []
33
+ }
34
+
35
+ /** The package that holds the dashboard role, connected or not. */
36
+ export function activeDashboard(): string | null {
37
+ return active
38
+ }
39
+
40
+ export function setActiveDashboard(packageName: string | null): void {
41
+ if (active === packageName) return
42
+ active = packageName
43
+ emit()
24
44
  }
25
45
 
26
- export function addOverlayApp(packageName: string): void {
27
- if (apps.has(packageName)) return
28
- apps.add(packageName)
46
+ export function addDashboardApp(packageName: string): void {
47
+ if (connected.has(packageName)) return
48
+ connected.add(packageName)
29
49
  emit()
30
50
  }
31
51
 
32
- export function removeOverlayApp(packageName: string): void {
33
- viewers.delete(packageName)
34
- if (!apps.delete(packageName)) return
52
+ export function removeDashboardApp(packageName: string): void {
53
+ listening.delete(packageName)
54
+ if (!connected.delete(packageName)) return
35
55
  emit()
36
56
  }
37
57
 
38
- /** Record whether an overlay app subscribes to phone notifications (it is a viewer while it does). */
39
- export function setOverlayViewer(packageName: string, listening: boolean): void {
40
- const was = viewers.has(packageName)
41
- if (listening && apps.has(packageName)) viewers.add(packageName)
42
- else viewers.delete(packageName)
43
- if (was !== viewers.has(packageName)) emit()
58
+ /** Record whether a dashboard subscribes to phone notifications (the overlay app is the viewer while it does). */
59
+ export function setOverlayViewer(packageName: string, subscribed: boolean): void {
60
+ const was = overlayViewerRunning()
61
+ if (subscribed && connected.has(packageName)) listening.add(packageName)
62
+ else listening.delete(packageName)
63
+ if (was !== overlayViewerRunning()) emit()
44
64
  }
45
65
 
46
- /** An overlay app that presents phone notifications is running. */
66
+ /** The overlay app presents phone notifications. */
47
67
  export function overlayViewerRunning(): boolean {
48
- return viewers.size > 0
68
+ return active !== null && connected.has(active) && listening.has(active)
49
69
  }
50
70
 
51
71
  export function onOverlayAppsChange(listener: () => void): () => void {
@@ -55,9 +75,9 @@ export function onOverlayAppsChange(listener: () => void): () => void {
55
75
  }
56
76
  }
57
77
 
58
- /** The views `packageName` may draw: main for everyone, the dashboard for overlay apps on eligible glasses. */
78
+ /** The views `packageName` may draw: main for everyone, the dashboard for the overlay app on eligible glasses. */
59
79
  export function displayViewsFor(packageName: string, eligible: boolean): DisplayView[] {
60
- return apps.has(packageName) && eligible ? ["main", "dashboard"] : ["main"]
80
+ return isOverlayApp(packageName) && eligible ? ["main", "dashboard"] : ["main"]
61
81
  }
62
82
 
63
83
  function emit(): void {
@@ -66,6 +86,7 @@ function emit(): void {
66
86
 
67
87
  /** Tests only. */
68
88
  export function _resetOverlayAppsForTest(): void {
69
- apps.clear()
70
- viewers.clear()
89
+ connected.clear()
90
+ listening.clear()
91
+ active = null
71
92
  }
@@ -1,28 +1,23 @@
1
1
  /**
2
2
  * The settings bridge's vocabulary (session.settings): which engine settings
3
- * a system miniapp may read or write, the derived read-only keys the engine
4
- * computes rather than stores, and the request validation. Pure, so the
5
- * runtime's handlers stay thin and this can be tested without the native
6
- * import surface the runtime pulls in.
3
+ * a miniapp with the PLATFORM_SETTINGS permission may read or write, the
4
+ * derived read-only keys the engine computes rather than stores, and the
5
+ * request validation. Pure, so the runtime's handlers stay thin and this can
6
+ * be tested without the native import surface the runtime pulls in.
7
7
  *
8
- * The dashboard miniapp is the consumer: it edits the same dashboard,
9
- * notification and glasses-menu keys the phone's settings screens used to,
10
- * through the store, so persistence, server sync and the Bluetooth sync of
11
- * `menu_apps` behave as if a phone screen wrote them.
8
+ * A dashboard miniapp is the usual consumer: it edits the notification and
9
+ * glasses-menu keys the phone's settings screens used to, through the store,
10
+ * so persistence, server sync and the Bluetooth sync of `menu_apps` behave as
11
+ * if a phone screen wrote them. A dashboard's own look is not here; it keeps
12
+ * that in its storage.
12
13
  */
13
14
 
14
15
  export type SettingsBridgeAccess = "read" | "write"
15
16
 
16
- /** Stored settings the bridge serves, by key, with what a system app may do to each. */
17
+ /** Stored settings the bridge serves, by key, with what a permitted app may do to each. */
17
18
  export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>> = {
18
19
  // The dashboard's master switch is a phone-side debug knob.
19
20
  custom_dashboard_enabled: "read",
20
- custom_dashboard_clock_style: "write",
21
- custom_dashboard_clock_show_date: "write",
22
- custom_dashboard_battery_style: "write",
23
- custom_dashboard_layout: "write",
24
- custom_dashboard_calendar_event_count: "write",
25
- custom_dashboard_calendar_days_ahead: "write",
26
21
  contextual_dashboard: "write",
27
22
  twelve_hour_time: "write",
28
23
  menu_apps: "write",
@@ -38,18 +33,23 @@ export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>
38
33
 
39
34
  /**
40
35
  * Read-only keys the engine computes: whether the wearer can see the phone,
41
- * whether the foreground miniapp is drawing on the glasses, and the ring's
42
- * battery. They change on their own clocks; the runtime pushes them like
43
- * stored keys.
36
+ * whether the foreground miniapp is drawing on the glasses, the ring's
37
+ * battery, and the package that holds the dashboard role (null for nobody).
38
+ * They change on their own clocks; the runtime pushes them like stored keys.
44
39
  */
45
- export const DERIVED_SETTINGS_KEYS = ["phone_active", "foreground_app_on_screen", "ring_battery"] as const
40
+ export const DERIVED_SETTINGS_KEYS = [
41
+ "phone_active",
42
+ "foreground_app_on_screen",
43
+ "ring_battery",
44
+ "active_dashboard",
45
+ ] as const
46
46
  export type DerivedSettingsKey = (typeof DERIVED_SETTINGS_KEYS)[number]
47
47
 
48
48
  export function isDerivedSettingsKey(key: string): key is DerivedSettingsKey {
49
49
  return (DERIVED_SETTINGS_KEYS as readonly string[]).includes(key)
50
50
  }
51
51
 
52
- /** What a system app may do with `key`, or null when the bridge does not serve it. */
52
+ /** What a permitted app may do with `key`, or null when the bridge does not serve it. */
53
53
  export function settingsBridgeAccess(key: string): SettingsBridgeAccess | null {
54
54
  if (isDerivedSettingsKey(key)) return "read"
55
55
  return SETTINGS_BRIDGE_KEYS[key] ?? null
@@ -25,6 +25,7 @@ import {islandNotifications} from "../services/NotificationsEmitter"
25
25
  import sttModelManager from "../services/STTModelManager"
26
26
  import {miniappLauncher} from "../services/MiniappLauncher"
27
27
  import {miniappRunningRegistry} from "../services/MiniappRunningRegistry"
28
+ import {activeDashboardAmong, resolveActiveDashboard} from "../services/pages/activeDashboard"
28
29
  import {defaultHiddenStatus, isGatedByDevTools, isGatedUntilPaired, resolveHiddenStatus} from "./appVisibility"
29
30
  import {SETTINGS, useSettingsStore} from "./settings"
30
31
  import {PermissionFeatures, permissions} from "../facades/permissions"
@@ -102,6 +103,7 @@ interface AppStatusState {
102
103
  * off leaves it running. Persists the wearer's choice beside the hidden flag.
103
104
  */
104
105
  setAlwaysOn: (packageName: string, status: boolean) => Promise<void>
106
+ setActiveDashboard: (packageName: string) => Promise<void>
105
107
  /** Replace the whole apps array. Hosts use this when their extra source changes. */
106
108
  setApps: (apps: ClientApp[]) => void
107
109
  }
@@ -512,9 +514,17 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
512
514
 
513
515
  uninstall: (packageName, version) => {
514
516
  return Res.try_async(async () => {
517
+ const wasActiveDashboard = activeDashboardAmong(get().apps) === packageName
515
518
  const res = await appRegistry.uninstall(packageName, version)
516
519
  if (res.is_error()) throw res.error
517
520
  set((s) => ({apps: s.apps.filter((a) => a.packageName !== packageName)}))
521
+ // The dashboard role fell back to another dashboard, which was stopped
522
+ // when it lost the role: start it, as launch would have.
523
+ if (wasActiveDashboard) {
524
+ const apps = get().apps
525
+ const heir = apps.find((a) => a.packageName === activeDashboardAmong(apps))
526
+ if (heir?.alwaysOn && !heir.running) await get().start(heir, {skipNavigation: true})
527
+ }
518
528
  })
519
529
  },
520
530
 
@@ -559,6 +569,18 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
559
569
  if (status && !app.running) await get().start(app, {skipNavigation: true})
560
570
  },
561
571
 
572
+ // The dashboard role (ADR-0009) moves to `packageName`: the previous holder
573
+ // stops and the new one starts in the background.
574
+ setActiveDashboard: async (packageName: string) => {
575
+ const apps = get().apps
576
+ const next = apps.find((a) => a.packageName === packageName && a.type === "dashboard")
577
+ if (!next) return
578
+ const previous = apps.find((a) => a.packageName === activeDashboardAmong(apps))
579
+ await useSettingsStore.getState().setSetting(SETTINGS.active_dashboard.key, packageName)
580
+ if (previous?.running && previous.packageName !== packageName) await get().stop(previous.packageName)
581
+ if (!next.running) await get().start(next, {skipNavigation: true})
582
+ },
583
+
562
584
  setApps: (apps) => set({apps}),
563
585
  }))
564
586
 
@@ -665,8 +687,28 @@ export const useRunningAlwaysOnApps = () => {
665
687
  return useMemo(() => apps.filter((app) => app.alwaysOn && app.running), [apps])
666
688
  }
667
689
 
668
- /** Apps that run without a foreground surface: background ones and the system dashboard. */
669
- const isBackgroundApp = (app: ClientApp): boolean => app.type === "background" || app.type === "system_dashboard"
690
+ /** Installed miniapps that can hold the dashboard role: manifest type "dashboard". */
691
+ export const useDashboards = () => {
692
+ const apps = useApps()
693
+ return useMemo(() => apps.filter((app) => app.type === "dashboard"), [apps])
694
+ }
695
+
696
+ /** The miniapp that holds the dashboard role (ADR-0009), or null when none is installed. */
697
+ export const useActiveDashboard = (): ClientApp | null => {
698
+ const dashboards = useDashboards()
699
+ const chosen = useSettingsStore((s) => s.getSetting(SETTINGS.active_dashboard.key))
700
+ return useMemo(() => {
701
+ const packageName = resolveActiveDashboard(
702
+ dashboards.map((app) => app.packageName),
703
+ chosen,
704
+ SETTINGS.active_dashboard.defaultValue(),
705
+ )
706
+ return dashboards.find((app) => app.packageName === packageName) ?? null
707
+ }, [dashboards, chosen])
708
+ }
709
+
710
+ /** Apps that run without a foreground surface: background ones and dashboards. */
711
+ const isBackgroundApp = (app: ClientApp): boolean => app.type === "background" || app.type === "dashboard"
670
712
 
671
713
  export const useActiveBackgroundApps = () => {
672
714
  const apps = useApps()
@@ -794,51 +794,12 @@ export const SETTINGS: Record<string, Setting> = {
794
794
  saveOnServer: false,
795
795
  persist: true,
796
796
  },
797
- // Device-local preferences for the Scene API dashboard available on G2
798
- // custom firmware. Eligibility is firmware-reported at runtime, so there is
799
- // deliberately no enable toggle: eligible devices replace the native
800
- // dashboard automatically while stock firmware keeps its native dashboard.
801
- custom_dashboard_clock_style: {
802
- key: "custom_dashboard_clock_style",
803
- defaultValue: () => "compact",
804
- writable: true,
805
- saveOnServer: false,
806
- persist: true,
807
- },
808
- custom_dashboard_clock_show_date: {
809
- key: "custom_dashboard_clock_show_date",
810
- defaultValue: () => true,
811
- writable: true,
812
- saveOnServer: false,
813
- persist: true,
814
- },
815
- // "solid" | "two_tone": the battery widgets' G and R.
816
- custom_dashboard_battery_style: {
817
- key: "custom_dashboard_battery_style",
818
- defaultValue: () => "solid",
819
- writable: true,
820
- saveOnServer: false,
821
- persist: true,
822
- },
823
- // {preset, slots: {slotId: widgetId}, scratchpad: [widgetId]} — the
824
- // dashboard miniapp owns the shape and its default; null draws its default layout.
825
- custom_dashboard_layout: {
826
- key: "custom_dashboard_layout",
827
- defaultValue: () => null,
828
- writable: true,
829
- saveOnServer: false,
830
- persist: true,
831
- },
832
- custom_dashboard_calendar_event_count: {
833
- key: "custom_dashboard_calendar_event_count",
834
- defaultValue: () => 3,
835
- writable: true,
836
- saveOnServer: false,
837
- persist: true,
838
- },
839
- custom_dashboard_calendar_days_ahead: {
840
- key: "custom_dashboard_calendar_days_ahead",
841
- defaultValue: () => 5,
797
+ // The wearer's choice of dashboard miniapp (ADR-0009). Installs are
798
+ // per-phone, so the choice is device-local. The default is the bundled
799
+ // dashboard, which the role falls back to when the choice is not installed.
800
+ active_dashboard: {
801
+ key: "active_dashboard",
802
+ defaultValue: () => "com.glassly.dashboard",
842
803
  writable: true,
843
804
  saveOnServer: false,
844
805
  persist: true,
@@ -10,7 +10,7 @@ import type {CompatibilityResult} from "../utils/hardware/hardware"
10
10
  /**
11
11
  * App execution model types
12
12
  */
13
- export type AppletType = "standard" | "background" | "system_dashboard"
13
+ export type AppletType = "standard" | "background" | "dashboard"
14
14
 
15
15
  /**
16
16
  * Permission types that apps can request
@@ -29,6 +29,8 @@ export type AppPermissionType =
29
29
  | "MEMORY"
30
30
  | "AI"
31
31
  | "SPEAKERS"
32
+ | "MANAGE_MINIAPPS"
33
+ | "PLATFORM_SETTINGS"
32
34
 
33
35
  /**
34
36
  * Permission object with type and description