@glassly/engine 0.3.5-beta.760 → 0.3.5-beta.775

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 (47) hide show
  1. package/build/engine.d.ts +3 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/memory.d.ts +10 -2
  4. package/build/facades/memory.d.ts.map +1 -1
  5. package/build/facades/memory.js +10 -0
  6. package/build/facades/memory.js.map +1 -1
  7. package/build/generated/releaseMetadata.js +3 -3
  8. package/build/generated/releaseMetadata.js.map +1 -1
  9. package/build/services/BlobStore.js +3 -3
  10. package/build/services/BlobStore.js.map +1 -1
  11. package/build/services/ForegroundLocationPermission.d.ts +1 -1
  12. package/build/services/ForegroundLocationPermission.d.ts.map +1 -1
  13. package/build/services/ForegroundLocationPermission.js.map +1 -1
  14. package/build/services/LocalMiniappRuntime.d.ts +4 -2
  15. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  16. package/build/services/LocalMiniappRuntime.js +51 -11
  17. package/build/services/LocalMiniappRuntime.js.map +1 -1
  18. package/build/services/MemoryStore.d.ts +13 -1
  19. package/build/services/MemoryStore.d.ts.map +1 -1
  20. package/build/services/MemoryStore.js +98 -1
  21. package/build/services/MemoryStore.js.map +1 -1
  22. package/build/services/manifestActions.d.ts +1 -1
  23. package/build/services/manifestActions.d.ts.map +1 -1
  24. package/build/services/manifestActions.js +3 -2
  25. package/build/services/manifestActions.js.map +1 -1
  26. package/build/services/memoryBackupFile.d.ts +9 -0
  27. package/build/services/memoryBackupFile.d.ts.map +1 -0
  28. package/build/services/memoryBackupFile.js +47 -0
  29. package/build/services/memoryBackupFile.js.map +1 -0
  30. package/build/services/widgetPlacements.d.ts +8 -7
  31. package/build/services/widgetPlacements.d.ts.map +1 -1
  32. package/build/services/widgetPlacements.js +7 -6
  33. package/build/services/widgetPlacements.js.map +1 -1
  34. package/build/types/applet.d.ts +3 -1
  35. package/build/types/applet.d.ts.map +1 -1
  36. package/build/types/applet.js.map +1 -1
  37. package/package.json +9 -9
  38. package/src/facades/memory.ts +11 -2
  39. package/src/generated/releaseMetadata.ts +3 -3
  40. package/src/services/BlobStore.ts +3 -3
  41. package/src/services/ForegroundLocationPermission.ts +1 -1
  42. package/src/services/LocalMiniappRuntime.ts +56 -11
  43. package/src/services/MemoryStore.ts +111 -2
  44. package/src/services/manifestActions.ts +10 -3
  45. package/src/services/memoryBackupFile.ts +50 -0
  46. package/src/services/widgetPlacements.ts +23 -9
  47. package/src/types/applet.ts +3 -1
@@ -2,8 +2,8 @@
2
2
  * Who placed which widget where: the pure bookkeeping behind the runtime's
3
3
  * widget routing. A placement is one host's box for one provider widget;
4
4
  * a provider is placed while any host holds one of its widgets. Frames are
5
- * kept per widget so a host that places one again gets the last frame
6
- * at once.
5
+ * kept per widget and view so a host that places one again gets the last
6
+ * card and expanded frames at once.
7
7
  */
8
8
  export const widgetKey = (providerPackageName, widgetId) => `${providerPackageName}/${widgetId}`;
9
9
  const placementKey = (hostPackageName, providerPackageName, widgetId) => `${hostPackageName}|${widgetKey(providerPackageName, widgetId)}`;
@@ -41,13 +41,14 @@ export function dropProvider(state, providerPackageName) {
41
41
  }
42
42
  return placements.size === state.placements.size ? state : { ...state, placements };
43
43
  }
44
- export function setFrame(state, providerPackageName, widgetId, elements) {
44
+ export function setFrame(state, providerPackageName, widgetId, elements, view = "card") {
45
+ const key = widgetKey(providerPackageName, widgetId);
45
46
  const frames = new Map(state.frames);
46
- frames.set(widgetKey(providerPackageName, widgetId), elements);
47
+ frames.set(key, { ...state.frames.get(key), [view]: elements });
47
48
  return { ...state, frames };
48
49
  }
49
- export function frameOf(state, providerPackageName, widgetId) {
50
- return state.frames.get(widgetKey(providerPackageName, widgetId)) ?? null;
50
+ export function frameOf(state, providerPackageName, widgetId, view = "card") {
51
+ return state.frames.get(widgetKey(providerPackageName, widgetId))?.[view] ?? null;
51
52
  }
52
53
  export function placementOf(state, hostPackageName, providerPackageName, widgetId) {
53
54
  return state.placements.get(placementKey(hostPackageName, providerPackageName, widgetId)) ?? null;
@@ -1 +1 @@
1
- {"version":3,"file":"widgetPlacements.js","sourceRoot":"","sources":["../../src/services/widgetPlacements.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Who placed which widget where: the pure bookkeeping behind the runtime's\n * widget routing. A placement is one host's box for one provider widget;\n * a provider is placed while any host holds one of its widgets. Frames are\n * kept per widget so a host that places one again gets the last frame\n * at once.\n */\n\nexport interface WidgetBox {\n size: \"XS\" | \"SM\" | \"MD\" | \"LG\"\n width: number\n height: number\n}\n\nexport interface Placement {\n hostPackageName: string\n providerPackageName: string\n widgetId: string\n box: WidgetBox\n}\n\nexport interface WidgetPlacements {\n /** Keyed by `${host}|${provider}/${widgetId}`. */\n placements: ReadonlyMap<string, Placement>\n /** The latest frame per `${provider}/${widgetId}`. */\n frames: ReadonlyMap<string, unknown[]>\n}\n\nexport const widgetKey = (providerPackageName: string, widgetId: string): string => `${providerPackageName}/${widgetId}`\nconst placementKey = (hostPackageName: string, providerPackageName: string, widgetId: string): string =>\n `${hostPackageName}|${widgetKey(providerPackageName, widgetId)}`\n\nexport function emptyWidgetPlacements(): WidgetPlacements {\n return {placements: new Map(), frames: new Map()}\n}\n\nexport function placeWidget(state: WidgetPlacements, placement: Placement): WidgetPlacements {\n const placements = new Map(state.placements)\n placements.set(placementKey(placement.hostPackageName, placement.providerPackageName, placement.widgetId), placement)\n return {...state, placements}\n}\n\nexport function unplaceWidget(\n state: WidgetPlacements,\n hostPackageName: string,\n providerPackageName: string,\n widgetId: string,\n): WidgetPlacements {\n const key = placementKey(hostPackageName, providerPackageName, widgetId)\n if (!state.placements.has(key)) return state\n const placements = new Map(state.placements)\n placements.delete(key)\n return {...state, placements}\n}\n\n/** A host went away: drop everything it placed. */\nexport function dropHost(state: WidgetPlacements, hostPackageName: string): WidgetPlacements {\n const placements = new Map(state.placements)\n for (const [key, placement] of state.placements) {\n if (placement.hostPackageName === hostPackageName) placements.delete(key)\n }\n return placements.size === state.placements.size ? state : {...state, placements}\n}\n\n/** A provider went away: drop its placements, keep its frames for the placeholder. */\nexport function dropProvider(state: WidgetPlacements, providerPackageName: string): WidgetPlacements {\n const placements = new Map(state.placements)\n for (const [key, placement] of state.placements) {\n if (placement.providerPackageName === providerPackageName) placements.delete(key)\n }\n return placements.size === state.placements.size ? state : {...state, placements}\n}\n\nexport function setFrame(state: WidgetPlacements, providerPackageName: string, widgetId: string, elements: unknown[]): WidgetPlacements {\n const frames = new Map(state.frames)\n frames.set(widgetKey(providerPackageName, widgetId), elements)\n return {...state, frames}\n}\n\nexport function frameOf(state: WidgetPlacements, providerPackageName: string, widgetId: string): unknown[] | null {\n return state.frames.get(widgetKey(providerPackageName, widgetId)) ?? null\n}\n\nexport function placementOf(\n state: WidgetPlacements,\n hostPackageName: string,\n providerPackageName: string,\n widgetId: string,\n): Placement | null {\n return state.placements.get(placementKey(hostPackageName, providerPackageName, widgetId)) ?? null\n}\n\n/** Hosts holding this widget, each once. */\nexport function hostsOf(state: WidgetPlacements, providerPackageName: string, widgetId: string): string[] {\n const hosts = new Set<string>()\n for (const placement of state.placements.values()) {\n if (placement.providerPackageName === providerPackageName && placement.widgetId === widgetId) {\n hosts.add(placement.hostPackageName)\n }\n }\n return Array.from(hosts)\n}\n\n/** Providers with at least one widget placed. */\nexport function placedProviders(state: WidgetPlacements): Set<string> {\n const providers = new Set<string>()\n for (const placement of state.placements.values()) providers.add(placement.providerPackageName)\n return providers\n}\n\nexport function isProviderPlaced(state: WidgetPlacements, providerPackageName: string): boolean {\n for (const placement of state.placements.values()) {\n if (placement.providerPackageName === providerPackageName) return true\n }\n return false\n}\n\n/** The placements a host holds on a provider, for re-delivering boxes. */\nexport function placementsBy(state: WidgetPlacements, hostPackageName: string): Placement[] {\n return Array.from(state.placements.values()).filter((placement) => placement.hostPackageName === hostPackageName)\n}\n"]}
1
+ {"version":3,"file":"widgetPlacements.js","sourceRoot":"","sources":["../../src/services/widgetPlacements.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Who placed which widget where: the pure bookkeeping behind the runtime's\n * widget routing. A placement is one host's box for one provider widget;\n * a provider is placed while any host holds one of its widgets. Frames are\n * kept per widget and view so a host that places one again gets the last\n * card and expanded frames at once.\n */\n\nexport type WidgetView = \"card\" | \"expanded\"\n\nexport interface WidgetBox {\n size: \"XS\" | \"XSM\" | \"SM\" | \"MD\" | \"LG\"\n width: number\n height: number\n}\n\nexport interface Placement {\n hostPackageName: string\n providerPackageName: string\n widgetId: string\n box: WidgetBox\n}\n\nexport interface WidgetPlacements {\n /** Keyed by `${host}|${provider}/${widgetId}`. */\n placements: ReadonlyMap<string, Placement>\n /** The latest frame per `${provider}/${widgetId}` and view. */\n frames: ReadonlyMap<string, Readonly<Partial<Record<WidgetView, unknown[]>>>>\n}\n\nexport const widgetKey = (providerPackageName: string, widgetId: string): string => `${providerPackageName}/${widgetId}`\nconst placementKey = (hostPackageName: string, providerPackageName: string, widgetId: string): string =>\n `${hostPackageName}|${widgetKey(providerPackageName, widgetId)}`\n\nexport function emptyWidgetPlacements(): WidgetPlacements {\n return {placements: new Map(), frames: new Map()}\n}\n\nexport function placeWidget(state: WidgetPlacements, placement: Placement): WidgetPlacements {\n const placements = new Map(state.placements)\n placements.set(placementKey(placement.hostPackageName, placement.providerPackageName, placement.widgetId), placement)\n return {...state, placements}\n}\n\nexport function unplaceWidget(\n state: WidgetPlacements,\n hostPackageName: string,\n providerPackageName: string,\n widgetId: string,\n): WidgetPlacements {\n const key = placementKey(hostPackageName, providerPackageName, widgetId)\n if (!state.placements.has(key)) return state\n const placements = new Map(state.placements)\n placements.delete(key)\n return {...state, placements}\n}\n\n/** A host went away: drop everything it placed. */\nexport function dropHost(state: WidgetPlacements, hostPackageName: string): WidgetPlacements {\n const placements = new Map(state.placements)\n for (const [key, placement] of state.placements) {\n if (placement.hostPackageName === hostPackageName) placements.delete(key)\n }\n return placements.size === state.placements.size ? state : {...state, placements}\n}\n\n/** A provider went away: drop its placements, keep its frames for the placeholder. */\nexport function dropProvider(state: WidgetPlacements, providerPackageName: string): WidgetPlacements {\n const placements = new Map(state.placements)\n for (const [key, placement] of state.placements) {\n if (placement.providerPackageName === providerPackageName) placements.delete(key)\n }\n return placements.size === state.placements.size ? state : {...state, placements}\n}\n\nexport function setFrame(\n state: WidgetPlacements,\n providerPackageName: string,\n widgetId: string,\n elements: unknown[],\n view: WidgetView = \"card\",\n): WidgetPlacements {\n const key = widgetKey(providerPackageName, widgetId)\n const frames = new Map(state.frames)\n frames.set(key, {...state.frames.get(key), [view]: elements})\n return {...state, frames}\n}\n\nexport function frameOf(\n state: WidgetPlacements,\n providerPackageName: string,\n widgetId: string,\n view: WidgetView = \"card\",\n): unknown[] | null {\n return state.frames.get(widgetKey(providerPackageName, widgetId))?.[view] ?? null\n}\n\nexport function placementOf(\n state: WidgetPlacements,\n hostPackageName: string,\n providerPackageName: string,\n widgetId: string,\n): Placement | null {\n return state.placements.get(placementKey(hostPackageName, providerPackageName, widgetId)) ?? null\n}\n\n/** Hosts holding this widget, each once. */\nexport function hostsOf(state: WidgetPlacements, providerPackageName: string, widgetId: string): string[] {\n const hosts = new Set<string>()\n for (const placement of state.placements.values()) {\n if (placement.providerPackageName === providerPackageName && placement.widgetId === widgetId) {\n hosts.add(placement.hostPackageName)\n }\n }\n return Array.from(hosts)\n}\n\n/** Providers with at least one widget placed. */\nexport function placedProviders(state: WidgetPlacements): Set<string> {\n const providers = new Set<string>()\n for (const placement of state.placements.values()) providers.add(placement.providerPackageName)\n return providers\n}\n\nexport function isProviderPlaced(state: WidgetPlacements, providerPackageName: string): boolean {\n for (const placement of state.placements.values()) {\n if (placement.providerPackageName === providerPackageName) return true\n }\n return false\n}\n\n/** The placements a host holds on a provider, for re-delivering boxes. */\nexport function placementsBy(state: WidgetPlacements, hostPackageName: string): Placement[] {\n return Array.from(state.placements.values()).filter((placement) => placement.hostPackageName === hostPackageName)\n}\n"]}
@@ -81,13 +81,15 @@ export interface DeclaredAction {
81
81
  */
82
82
  visibility?: "public" | "system";
83
83
  }
84
- export type DeclaredWidgetSize = "XS" | "SM" | "MD" | "LG";
84
+ export type DeclaredWidgetSize = "XS" | "XSM" | "SM" | "MD" | "LG";
85
85
  /** A dashboard widget a miniapp declares in `miniapp.json` and serves through `session.widgets`. */
86
86
  export interface DeclaredWidget {
87
87
  id: string;
88
88
  name: string;
89
89
  sizes: DeclaredWidgetSize[];
90
90
  defaultSize: DeclaredWidgetSize;
91
+ /** A tap on the selected widget expands it to the full screen instead of activating the card. */
92
+ expandable: boolean;
91
93
  }
92
94
  export interface ClientApp extends AppletInterface {
93
95
  offline: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"applet.d.ts","sourceRoot":"","sources":["../../src/types/applet.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"applet.d.ts","sourceRoot":"","sources":["../../src/types/applet.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"applet.js","sourceRoot":"","sources":["../../src/types/applet.ts"],"names":[],"mappings":"","sourcesContent":["import type {ManifestPermissionType} from \"@glassly/miniapp/manifest\"\nimport type {ReactNode} from \"react\"\n/**\n * App and miniapp types owned by @glassly/engine.\n */\n\nimport {HardwareRequirement} from \"./hardware\"\n\nimport type {CompatibilityResult} from \"../utils/hardware/hardware\"\n\n/**\n * App execution model types\n */\nexport type AppletType = \"standard\" | \"background\" | \"dashboard\"\n\n/**\n * Permission types that apps can request: the manifest vocabulary from the\n * SDK (the single source the CLI validator reads too) plus the host-only ALL.\n */\nexport type AppPermissionType = \"ALL\" | ManifestPermissionType\n\n/**\n * Permission object with type and description\n */\nexport interface AppletPermission {\n type: AppPermissionType\n description?: string\n required?: boolean\n}\n\n/**\n * Minimal app interface for client home screen display\n * Optimized for fast rendering - only essential fields\n *\n * This is the client-facing interface used by mobile apps.\n * Internal cloud services use AppI from models (more fields).\n */\nexport interface AppletInterface {\n packageName: string\n name: string\n webviewUrl: string\n logoUrl: string\n iconComponent?: ReactNode\n type: AppletType\n permissions: AppletPermission[]\n running: boolean\n /**\n * True while this app's UI WebView is mounted in the Compositor overlay.\n * At most one app is foregrounded at a time. Distinct from `running`:\n * a foregrounded app is always running, but a running app may be\n * backgrounded (JSContext alive, WebView torn down). Normalized to a\n * concrete boolean by the apps store's projectApps; literal call sites\n * may omit it.\n */\n foregrounded?: boolean\n healthy: boolean\n hardwareRequirements: HardwareRequirement[]\n /** ISO date string when the app was installed */\n installedDate?: string\n /** ISO date string when the app was last run. Undefined if never run (app is \"new\") */\n lastActiveAt?: string\n}\n\n/**\n * Runtime applet shape used by the engine apps store and OEM hosts.\n *\n * Extends AppletInterface with the host-side fields the manager / OEM tracks\n * outside the manifest: offline routing, hardware compatibility result,\n * loading/hidden/local flags, lifecycle hooks, screenshot, and the\n * dev-miniapp escape hatches (`devUrl` / `isMiniappDev`) that surface a\n * `glassly dev` snapshot.\n */\n/**\n * A manifest-declared action a miniapp exposes for other miniapps to invoke\n * via `session.actions`. Populated from `miniapp.json`'s `actions` field.\n * Shape maps 1:1 onto an MCP tool (id→name, description, parameters→inputSchema).\n */\nexport interface DeclaredAction {\n id: string\n description: string\n /** JSON-Schema input descriptor (MCP-compatible subset). */\n parameters?: Record<string, unknown>\n /** JSON-Schema descriptor for the action's structured result. */\n outputSchema?: Record<string, unknown>\n /**\n * Who may invoke the action: \"public\" (default — any installed miniapp) or\n * \"system\" (system miniapps such as the AI assistant only).\n */\n visibility?: \"public\" | \"system\"\n}\n\nexport type DeclaredWidgetSize = \"XS\" | \"SM\" | \"MD\" | \"LG\"\n\n/** A dashboard widget a miniapp declares in `miniapp.json` and serves through `session.widgets`. */\nexport interface DeclaredWidget {\n id: string\n name: string\n sizes: DeclaredWidgetSize[]\n defaultSize: DeclaredWidgetSize\n}\n\nexport interface ClientApp extends AppletInterface {\n offline: boolean\n offlineRoute: string\n compatibility?: CompatibilityResult\n loading: boolean\n local: boolean\n hidden: boolean\n /**\n * Always On miniapp: the host starts it at launch and it only stops from\n * the app switcher's Always On pill; its card cannot be swiped away and\n * the capsule close minimizes it. Manifest `alwaysOn` gives the default;\n * the wearer's choice persists beside the hidden flag.\n */\n alwaysOn?: boolean\n /** Running, but paused while the paired glasses are disconnected (MiniappPauseCoordinator). */\n paused?: boolean\n onStart?: () => void\n onStop?: () => void\n screenshot?: string\n runtimePermissions?: string[]\n declaredPermissions?: string[]\n version?: string\n /** Manifest `description`: the paragraph the store and the info page show. */\n description?: string\n /** Manifest `tagline`: one line under the name. */\n tagline?: string\n /** Manifest `tags`: lowercase store tag slugs. */\n tags?: string[]\n needsPcm?: boolean\n needsTranscript?: boolean\n devUrl?: string\n devPort?: number\n isMiniappDev?: boolean\n /** Manifest-declared actions (for session.miniapps.list + invoke gating). */\n actions?: DeclaredAction[]\n /** Manifest-declared dashboard widgets. */\n widgets?: DeclaredWidget[]\n /**\n * Running only because a host placed one of its widgets: no card in the\n * app switcher, no frame of its own on the glasses. Opening the app from\n * the home screen promotes it to a normal run.\n */\n widgetOnly?: boolean\n}\n"]}
1
+ {"version":3,"file":"applet.js","sourceRoot":"","sources":["../../src/types/applet.ts"],"names":[],"mappings":"","sourcesContent":["import type {ManifestPermissionType} from \"@glassly/miniapp/manifest\"\nimport type {ReactNode} from \"react\"\n/**\n * App and miniapp types owned by @glassly/engine.\n */\n\nimport {HardwareRequirement} from \"./hardware\"\n\nimport type {CompatibilityResult} from \"../utils/hardware/hardware\"\n\n/**\n * App execution model types\n */\nexport type AppletType = \"standard\" | \"background\" | \"dashboard\"\n\n/**\n * Permission types that apps can request: the manifest vocabulary from the\n * SDK (the single source the CLI validator reads too) plus the host-only ALL.\n */\nexport type AppPermissionType = \"ALL\" | ManifestPermissionType\n\n/**\n * Permission object with type and description\n */\nexport interface AppletPermission {\n type: AppPermissionType\n description?: string\n required?: boolean\n}\n\n/**\n * Minimal app interface for client home screen display\n * Optimized for fast rendering - only essential fields\n *\n * This is the client-facing interface used by mobile apps.\n * Internal cloud services use AppI from models (more fields).\n */\nexport interface AppletInterface {\n packageName: string\n name: string\n webviewUrl: string\n logoUrl: string\n iconComponent?: ReactNode\n type: AppletType\n permissions: AppletPermission[]\n running: boolean\n /**\n * True while this app's UI WebView is mounted in the Compositor overlay.\n * At most one app is foregrounded at a time. Distinct from `running`:\n * a foregrounded app is always running, but a running app may be\n * backgrounded (JSContext alive, WebView torn down). Normalized to a\n * concrete boolean by the apps store's projectApps; literal call sites\n * may omit it.\n */\n foregrounded?: boolean\n healthy: boolean\n hardwareRequirements: HardwareRequirement[]\n /** ISO date string when the app was installed */\n installedDate?: string\n /** ISO date string when the app was last run. Undefined if never run (app is \"new\") */\n lastActiveAt?: string\n}\n\n/**\n * Runtime applet shape used by the engine apps store and OEM hosts.\n *\n * Extends AppletInterface with the host-side fields the manager / OEM tracks\n * outside the manifest: offline routing, hardware compatibility result,\n * loading/hidden/local flags, lifecycle hooks, screenshot, and the\n * dev-miniapp escape hatches (`devUrl` / `isMiniappDev`) that surface a\n * `glassly dev` snapshot.\n */\n/**\n * A manifest-declared action a miniapp exposes for other miniapps to invoke\n * via `session.actions`. Populated from `miniapp.json`'s `actions` field.\n * Shape maps 1:1 onto an MCP tool (id→name, description, parameters→inputSchema).\n */\nexport interface DeclaredAction {\n id: string\n description: string\n /** JSON-Schema input descriptor (MCP-compatible subset). */\n parameters?: Record<string, unknown>\n /** JSON-Schema descriptor for the action's structured result. */\n outputSchema?: Record<string, unknown>\n /**\n * Who may invoke the action: \"public\" (default — any installed miniapp) or\n * \"system\" (system miniapps such as the AI assistant only).\n */\n visibility?: \"public\" | \"system\"\n}\n\nexport type DeclaredWidgetSize = \"XS\" | \"XSM\" | \"SM\" | \"MD\" | \"LG\"\n\n/** A dashboard widget a miniapp declares in `miniapp.json` and serves through `session.widgets`. */\nexport interface DeclaredWidget {\n id: string\n name: string\n sizes: DeclaredWidgetSize[]\n defaultSize: DeclaredWidgetSize\n /** A tap on the selected widget expands it to the full screen instead of activating the card. */\n expandable: boolean\n}\n\nexport interface ClientApp extends AppletInterface {\n offline: boolean\n offlineRoute: string\n compatibility?: CompatibilityResult\n loading: boolean\n local: boolean\n hidden: boolean\n /**\n * Always On miniapp: the host starts it at launch and it only stops from\n * the app switcher's Always On pill; its card cannot be swiped away and\n * the capsule close minimizes it. Manifest `alwaysOn` gives the default;\n * the wearer's choice persists beside the hidden flag.\n */\n alwaysOn?: boolean\n /** Running, but paused while the paired glasses are disconnected (MiniappPauseCoordinator). */\n paused?: boolean\n onStart?: () => void\n onStop?: () => void\n screenshot?: string\n runtimePermissions?: string[]\n declaredPermissions?: string[]\n version?: string\n /** Manifest `description`: the paragraph the store and the info page show. */\n description?: string\n /** Manifest `tagline`: one line under the name. */\n tagline?: string\n /** Manifest `tags`: lowercase store tag slugs. */\n tags?: string[]\n needsPcm?: boolean\n needsTranscript?: boolean\n devUrl?: string\n devPort?: number\n isMiniappDev?: boolean\n /** Manifest-declared actions (for session.miniapps.list + invoke gating). */\n actions?: DeclaredAction[]\n /** Manifest-declared dashboard widgets. */\n widgets?: DeclaredWidget[]\n /**\n * Running only because a host placed one of its widgets: no card in the\n * app switcher, no frame of its own on the glasses. Opening the app from\n * the home screen promotes it to a normal run.\n */\n widgetOnly?: boolean\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.5-beta.760",
3
+ "version": "0.3.5-beta.775",
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.5-beta.760",
101
- "@glassly/cloud-client": "0.3.5-beta.760",
102
- "@glassly/cloud-protocol": "0.3.5-beta.760",
103
- "@glassly/crust": "0.3.5-beta.760",
104
- "@glassly/miniapp": "0.3.5-beta.760",
100
+ "@glassly/bluetooth-sdk": "0.3.5-beta.775",
101
+ "@glassly/cloud-client": "0.3.5-beta.775",
102
+ "@glassly/cloud-protocol": "0.3.5-beta.775",
103
+ "@glassly/crust": "0.3.5-beta.775",
104
+ "@glassly/miniapp": "0.3.5-beta.775",
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.5-beta.760",
110
+ "@glassly/llm-providers": "0.3.5-beta.775",
111
111
  "@huggingface/jinja": "^0.5.9",
112
112
  "jsonrepair": "^3.12.0"
113
113
  },
@@ -115,8 +115,8 @@
115
115
  "@glassly/llm-providers": "workspace:*",
116
116
  "@types/semver": "^7.7.1",
117
117
  "expo-module-scripts": "^56.0.3",
118
- "expo-modules-core": "^57.0.7",
119
- "react-native-nitro-modules": "^0.35.10",
118
+ "expo-modules-core": "^58.0.23",
119
+ "react-native-nitro-modules": "^0.37.1",
120
120
  "typescript": "~5.9.2"
121
121
  },
122
122
  "peerDependencies": {
@@ -4,9 +4,10 @@
4
4
  * purge everything; the full timeline surface (lines, sessions, summaries)
5
5
  * is what miniapps reach through session.memory with the MEMORY permission.
6
6
  */
7
- import type {MemoryEntry, MemorySession} from "@glassly/miniapp"
7
+ import type {MemoryEntry, MemoryExportResult, MemoryImportResult, MemorySession} from "@glassly/miniapp"
8
8
 
9
- import type {MemoryStats} from "../services/MemoryStore"
9
+ import {exportMemoryFile, importMemoryFile} from "../services/memoryBackupFile"
10
+ import {MemoryBackupError, type MemoryStats} from "../services/MemoryStore"
10
11
  import {memoryStore} from "../services/memoryStoreInstance"
11
12
 
12
13
  export const memory = {
@@ -30,6 +31,14 @@ export const memory = {
30
31
  stats: (): Promise<MemoryStats> => memoryStore.stats(),
31
32
  /** Drop the entire timeline: every entry and every summary. */
32
33
  clearAll: (): Promise<void> => memoryStore.clear(),
34
+ /** Share the whole timeline as a JSON backup file. */
35
+ exportFile: (): Promise<MemoryExportResult> => exportMemoryFile(),
36
+ /**
37
+ * Pick a backup file and merge it in, skipping what's already stored.
38
+ * Resolves null on cancel; rejects with a MemoryBackupError for a non-backup file.
39
+ */
40
+ importFile: (): Promise<MemoryImportResult | null> => importMemoryFile(),
41
+ isBackupError: (err: unknown): boolean => err instanceof MemoryBackupError,
33
42
  /** Subscribe to store mutations (from miniapps or Settings). */
34
43
  onChanged: (listener: () => void): (() => void) => memoryStore.onChanged(listener),
35
44
  }
@@ -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.5",
15
- "releaseIdentity": "0.3.5-beta.760",
16
- "releaseSetId": "glassly-0.3.5-beta.760",
17
- "sourceCommit": "d5e2a8877bc5441b410677c79d96b38b985d5f91",
15
+ "releaseIdentity": "0.3.5-beta.775",
16
+ "releaseSetId": "glassly-0.3.5-beta.775",
17
+ "sourceCommit": "c48886d82edd42a7d012af3b8936251ac92ba949",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -383,7 +383,7 @@ export class BlobStore {
383
383
  return
384
384
  }
385
385
  up.handle.offset = payload.offset
386
- up.handle.writeBytes(bytes)
386
+ up.handle.writeBytesSync(bytes)
387
387
  up.handle.offset = up.bytes // restore append position
388
388
  this.hooks.sendResult(packageName, requestId, true, {bytesWritten: up.bytes})
389
389
  return
@@ -397,7 +397,7 @@ export class BlobStore {
397
397
  return
398
398
  }
399
399
  up.handle.offset = up.bytes
400
- up.handle.writeBytes(bytes)
400
+ up.handle.writeBytesSync(bytes)
401
401
  up.bytes += bytes.length
402
402
  this.hooks.sendResult(packageName, requestId, true, {bytesWritten: up.bytes})
403
403
  } catch (err) {
@@ -633,7 +633,7 @@ export class BlobStore {
633
633
  }
634
634
  try {
635
635
  const want = Math.max(0, Math.min(Number(payload.maxBytes) || 1024 * 1024, reader.remaining))
636
- const bytes = want > 0 ? reader.handle.readBytes(want) : new Uint8Array(0)
636
+ const bytes = want > 0 ? reader.handle.readBytesSync(want) : new Uint8Array(0)
637
637
  reader.remaining -= bytes.length
638
638
  const done = reader.remaining <= 0
639
639
  const base64 = bytes.length
@@ -13,7 +13,7 @@ export interface ForegroundLocationPermissionClient {
13
13
  */
14
14
  export async function resolveForegroundLocationPermission(
15
15
  client: ForegroundLocationPermissionClient,
16
- getAppState: () => string,
16
+ getAppState: () => string | null | undefined,
17
17
  ): Promise<ForegroundLocationPermissionResponse> {
18
18
  const current = await client.getForegroundPermissionsAsync()
19
19
  if (current.status === "granted" || getAppState() !== "active") {
@@ -109,6 +109,7 @@ import {
109
109
  unplaceWidget,
110
110
  type WidgetBox,
111
111
  type WidgetPlacements,
112
+ type WidgetView,
112
113
  } from "./widgetPlacements"
113
114
  import {
114
115
  activeDashboard,
@@ -183,6 +184,8 @@ import {listPhoneCalendarEvents, PhoneCalendarError} from "./PhoneCalendarServic
183
184
  import {LocalMiniappStorage} from "./LocalMiniappStorage"
184
185
  import {captureDecisionFor, type CaptureCandidate} from "./MemoryCapture"
185
186
  import {memoryStore} from "./memoryStoreInstance"
187
+ import {exportMemoryFile, importMemoryFile} from "./memoryBackupFile"
188
+ import {MemoryBackupError} from "./MemoryStore"
186
189
  import {speakerProfileStore} from "./speakerProfileStoreInstance"
187
190
 
188
191
  // =============================================================================
@@ -1591,6 +1594,8 @@ class LocalMiniappRuntime {
1591
1594
  case MiniappRequestType.MEMORY_QUERY:
1592
1595
  case MiniappRequestType.MEMORY_WINDOW:
1593
1596
  case MiniappRequestType.MEMORY_EXPORT_TEXT:
1597
+ case MiniappRequestType.MEMORY_EXPORT_FILE:
1598
+ case MiniappRequestType.MEMORY_IMPORT_FILE:
1594
1599
  case MiniappRequestType.MEMORY_SUMMARY_ADD:
1595
1600
  case MiniappRequestType.MEMORY_SUMMARY_LIST:
1596
1601
  case MiniappRequestType.MEMORY_PENDING_SUMMARY_HOUR:
@@ -4006,6 +4011,14 @@ class LocalMiniappRuntime {
4006
4011
  this.sendResult(packageName, requestId, true, {text: await memoryStore.exportText(scope, num(payload.at))})
4007
4012
  return
4008
4013
  }
4014
+ case MiniappRequestType.MEMORY_EXPORT_FILE: {
4015
+ this.sendResult(packageName, requestId, true, await exportMemoryFile())
4016
+ return
4017
+ }
4018
+ case MiniappRequestType.MEMORY_IMPORT_FILE: {
4019
+ this.sendResult(packageName, requestId, true, {imported: await importMemoryFile()})
4020
+ return
4021
+ }
4009
4022
  case MiniappRequestType.MEMORY_SUMMARY_ADD: {
4010
4023
  const hourStart = num(payload.hourStart)
4011
4024
  if (hourStart == null || typeof payload.text !== "string") {
@@ -4041,6 +4054,13 @@ class LocalMiniappRuntime {
4041
4054
  })
4042
4055
  }
4043
4056
  } catch (err) {
4057
+ if (err instanceof MemoryBackupError) {
4058
+ this.sendResult(packageName, requestId, false, undefined, {
4059
+ code: MiniappErrorCode.INVALID_ARGUMENT,
4060
+ message: err.message,
4061
+ })
4062
+ return
4063
+ }
4044
4064
  console.error(`${LOG_TAG}: ${requestType} error:`, err)
4045
4065
  this.sendResult(packageName, requestId, false, undefined, {
4046
4066
  code: MiniappErrorCode.INTERNAL,
@@ -4662,7 +4682,7 @@ class LocalMiniappRuntime {
4662
4682
  if (base64) {
4663
4683
  // File share via base64 — write to temp file then share
4664
4684
  const tempFile = new File(Paths.cache, filename || "shared_file")
4665
- tempFile.write(base64, {encoding: "base64"})
4685
+ tempFile.writeSync(base64, {encoding: "base64"})
4666
4686
  await Share.open({
4667
4687
  url: tempFile.uri,
4668
4688
  type: mimeType || "application/octet-stream",
@@ -4742,7 +4762,7 @@ class LocalMiniappRuntime {
4742
4762
  let file: File
4743
4763
  if (base64) {
4744
4764
  file = new File(Paths.cache, name)
4745
- file.write(base64, {encoding: "base64"})
4765
+ file.writeSync(base64, {encoding: "base64"})
4746
4766
  } else if (url) {
4747
4767
  file = await File.downloadFileAsync(url, new File(Paths.cache, name), {idempotent: true})
4748
4768
  } else {
@@ -6298,6 +6318,8 @@ class LocalMiniappRuntime {
6298
6318
  private widgetCallSeq = 0
6299
6319
  /** Elements per widget frame; a card is small, and the link is slow. */
6300
6320
  private static readonly WIDGET_FRAME_ELEMENT_CAP = 64
6321
+ /** Elements per expanded widget frame: the full screen, on the same link. */
6322
+ private static readonly WIDGET_EXPANDED_FRAME_ELEMENT_CAP = 128
6301
6323
 
6302
6324
  private requireWidgetHost(packageName: string, requestId: string | undefined, op: string): boolean {
6303
6325
  return this.requirePermission(packageName, requestId, "HOST_WIDGETS", `to ${op} widgets`)
@@ -6346,6 +6368,7 @@ class LocalMiniappRuntime {
6346
6368
  name: widget.name,
6347
6369
  sizes: widget.sizes,
6348
6370
  defaultSize: widget.defaultSize,
6371
+ expandable: widget.expandable,
6349
6372
  running: app.running,
6350
6373
  compatible: app.compatibility?.isCompatible !== false,
6351
6374
  })
@@ -6357,7 +6380,8 @@ class LocalMiniappRuntime {
6357
6380
  private parseWidgetBox(raw: unknown): WidgetBox | null {
6358
6381
  const box = raw as {size?: unknown; width?: unknown; height?: unknown} | null
6359
6382
  if (!box || typeof box !== "object") return null
6360
- if (box.size !== "XS" && box.size !== "SM" && box.size !== "MD" && box.size !== "LG") return null
6383
+ if (box.size !== "XS" && box.size !== "XSM" && box.size !== "SM" && box.size !== "MD" && box.size !== "LG")
6384
+ return null
6361
6385
  const width = Number(box.width)
6362
6386
  const height = Number(box.height)
6363
6387
  if (!(width > 0) || !(height > 0)) return null
@@ -6383,7 +6407,7 @@ class LocalMiniappRuntime {
6383
6407
  /**
6384
6408
  * session.widgets.place: record the host's box, wake the provider in widget
6385
6409
  * mode when it is not running, hand it the box, and answer with the last
6386
- * frame the provider pushed for the widget.
6410
+ * card and expanded frames the provider pushed for the widget.
6387
6411
  */
6388
6412
  private async handleWidgetPlace(
6389
6413
  hostPackageName: string,
@@ -6432,7 +6456,10 @@ class LocalMiniappRuntime {
6432
6456
  if (placementOf(this.widgetPlacements, hostPackageName, provider, widgetId) && this.connectedApps.has(provider)) {
6433
6457
  this.sendWidgetCall(provider, widgetId, {kind: "box", box})
6434
6458
  }
6435
- this.sendResult(hostPackageName, requestId, true, {frame: widgetFrameOf(this.widgetPlacements, provider, widgetId)})
6459
+ this.sendResult(hostPackageName, requestId, true, {
6460
+ frame: widgetFrameOf(this.widgetPlacements, provider, widgetId),
6461
+ expandedFrame: widgetFrameOf(this.widgetPlacements, provider, widgetId, "expanded"),
6462
+ })
6436
6463
  }
6437
6464
 
6438
6465
  private async handleWidgetUnplace(
@@ -6550,20 +6577,38 @@ class LocalMiniappRuntime {
6550
6577
  }
6551
6578
  }
6552
6579
 
6553
- /** Provider → host, one-shot: cache the frame and push it to every host holding the widget. */
6580
+ /** Provider → host, one-shot: cache the frame for its view and push it to every host holding the widget. */
6554
6581
  private handleWidgetUpdate(providerPackageName: string, payload: Record<string, unknown>): void {
6555
6582
  const widgetId = payload.widgetId as string | undefined
6556
6583
  const elements = payload.elements
6557
6584
  if (!widgetId || !Array.isArray(elements)) return
6558
6585
  const app = this.interopApps().find((a) => a.packageName === providerPackageName)
6559
- if (!app || !this.declaredWidget(app, widgetId)) return
6560
- if (elements.length > LocalMiniappRuntime.WIDGET_FRAME_ELEMENT_CAP || this.actionPayloadTooLarge(elements)) {
6561
- console.warn(`${LOG_TAG}: dropping oversized widget frame from ${providerPackageName}/${widgetId}`)
6586
+ const widget = app ? this.declaredWidget(app, widgetId) : undefined
6587
+ if (!widget) return
6588
+ const view: WidgetView = payload.view === "expanded" ? "expanded" : "card"
6589
+ if (view === "expanded" && !widget.expandable) {
6590
+ console.warn(
6591
+ `${LOG_TAG}: dropping expanded frame from ${providerPackageName}/${widgetId}: not declared expandable`,
6592
+ )
6562
6593
  return
6563
6594
  }
6564
- this.widgetPlacements = setWidgetFrame(this.widgetPlacements, providerPackageName, widgetId, elements)
6595
+ const cap =
6596
+ view === "expanded"
6597
+ ? LocalMiniappRuntime.WIDGET_EXPANDED_FRAME_ELEMENT_CAP
6598
+ : LocalMiniappRuntime.WIDGET_FRAME_ELEMENT_CAP
6599
+ if (elements.length > cap || this.actionPayloadTooLarge(elements)) {
6600
+ console.warn(`${LOG_TAG}: dropping oversized ${view} widget frame from ${providerPackageName}/${widgetId}`)
6601
+ return
6602
+ }
6603
+ this.widgetPlacements = setWidgetFrame(this.widgetPlacements, providerPackageName, widgetId, elements, view)
6565
6604
  for (const host of widgetHostsOf(this.widgetPlacements, providerPackageName, widgetId)) {
6566
- this.sendToMiniapp(host, {type: MiniappResponseType.WIDGET_FRAME, packageName: providerPackageName, widgetId, elements})
6605
+ this.sendToMiniapp(host, {
6606
+ type: MiniappResponseType.WIDGET_FRAME,
6607
+ packageName: providerPackageName,
6608
+ widgetId,
6609
+ elements,
6610
+ ...(view === "expanded" ? {view} : {}),
6611
+ })
6567
6612
  }
6568
6613
  }
6569
6614
 
@@ -17,7 +17,14 @@
17
17
  * user-scoped keys so existing history survives the move.
18
18
  */
19
19
 
20
- import type {MemoryEntry, MemoryNoteSource, MemorySession, MemorySummary} from "@glassly/miniapp"
20
+ import type {
21
+ MemoryBackup,
22
+ MemoryEntry,
23
+ MemoryImportResult,
24
+ MemoryNoteSource,
25
+ MemorySession,
26
+ MemorySummary,
27
+ } from "@glassly/miniapp"
21
28
 
22
29
  import {searchMemoryEntries} from "../utils/memory/search"
23
30
 
@@ -91,6 +98,14 @@ export interface MemoryStats {
91
98
  oldestAt: number | null
92
99
  }
93
100
 
101
+ /** Thrown by importBackup for anything that isn't a backup this build can read. */
102
+ export class MemoryBackupError extends Error {
103
+ constructor(message: string) {
104
+ super(message)
105
+ this.name = "MemoryBackupError"
106
+ }
107
+ }
108
+
94
109
  export function hourStartOf(ms: number): number {
95
110
  return Math.floor(ms / HOUR_MS) * HOUR_MS
96
111
  }
@@ -359,6 +374,65 @@ export class MemoryStore {
359
374
  return Math.max(...hours)
360
375
  }
361
376
 
377
+ /** The whole timeline as a backup file body (Settings > Memory, session.memory.exportFile). */
378
+ async exportBackup(): Promise<MemoryBackup> {
379
+ await this.ensureLoaded()
380
+ return {
381
+ format: "glassly-memory",
382
+ version: 1,
383
+ exportedAt: Date.now(),
384
+ entries: [...this.entries],
385
+ summaries: [...this.summaries].sort((a, b) => a.hourStart - b.hourStart),
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Merge a backup into the timeline. Skips entries whose id is stored, notes
391
+ * whose text is already in the same hour, and hours that already have a
392
+ * summary. Throws MemoryBackupError when `data` isn't a memory backup.
393
+ */
394
+ async importBackup(data: unknown): Promise<MemoryImportResult> {
395
+ const backup = parseBackup(data)
396
+ await this.ensureLoaded()
397
+ const result: MemoryImportResult = {entries: 0, summaries: 0, skipped: 0}
398
+
399
+ const ids = new Set(this.entries.map((e) => e.id))
400
+ const noteKeys = new Set(this.entries.filter((e) => e.kind === "note").map(noteKey))
401
+ for (const entry of backup.entries) {
402
+ const key = entry.kind === "note" ? noteKey(entry) : null
403
+ if (ids.has(entry.id) || (key != null && noteKeys.has(key))) {
404
+ result.skipped++
405
+ continue
406
+ }
407
+ ids.add(entry.id)
408
+ if (key != null) noteKeys.add(key)
409
+ this.entries.push(entry)
410
+ result.entries++
411
+ }
412
+
413
+ const byHour = new Map(this.summaries.map((s) => [s.hourStart, s]))
414
+ for (const summary of backup.summaries) {
415
+ if (byHour.get(summary.hourStart)?.text.trim()) {
416
+ result.skipped++
417
+ continue
418
+ }
419
+ byHour.set(summary.hourStart, summary)
420
+ result.summaries++
421
+ }
422
+
423
+ if (result.entries > 0) {
424
+ this.entries.sort((a, b) => a.at - b.at)
425
+ this.prune()
426
+ await this.persistLog()
427
+ }
428
+ if (result.summaries > 0) {
429
+ this.summaries = [...byHour.values()].sort((a, b) => a.hourStart - b.hourStart).slice(-MEMORY_BOUNDS.maxSummaries)
430
+ await this.persistSummaries()
431
+ }
432
+ if (result.entries > 0 || result.summaries > 0) this.emitChanged()
433
+ return result
434
+ }
435
+
362
436
  /** Plain-text export of a scope, for the OS share sheet. */
363
437
  async exportText(scope: "session" | "day" | "all", at?: number): Promise<string> {
364
438
  const sessions = (await this.sessionList()).filter((s) => {
@@ -563,10 +637,45 @@ function parseArray(raw: unknown): unknown[] {
563
637
  function normalize(text: string): string {
564
638
  return text
565
639
  .toLowerCase()
566
- .replace(/[^a-z0-9]+/g, " ")
640
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
567
641
  .trim()
568
642
  }
569
643
 
644
+ function noteKey(entry: MemoryEntry): string {
645
+ return `${hourStartOf(entry.at)}:${normalize(entry.text)}`
646
+ }
647
+
648
+ function parseBackup(data: unknown): {entries: MemoryEntry[]; summaries: MemorySummary[]} {
649
+ const raw = data as Record<string, unknown> | null
650
+ if (!raw || typeof raw !== "object" || raw.format !== "glassly-memory" || !Array.isArray(raw.entries)) {
651
+ throw new MemoryBackupError("Not a Glassly memory backup")
652
+ }
653
+ if (typeof raw.version !== "number" || raw.version > 1) {
654
+ throw new MemoryBackupError(`Unsupported memory backup version ${String(raw.version)}`)
655
+ }
656
+ const entries: MemoryEntry[] = []
657
+ for (const v of raw.entries) {
658
+ if (!isEntry(v) || !v.id || !v.text.trim() || !Number.isFinite(v.at)) continue
659
+ entries.push({
660
+ id: v.id,
661
+ kind: v.kind,
662
+ text: v.text.trim(),
663
+ at: v.at,
664
+ speakerId: typeof v.speakerId === "string" ? v.speakerId : null,
665
+ ...(typeof v.resolvedSpeakerName === "string" && v.resolvedSpeakerName
666
+ ? {resolvedSpeakerName: v.resolvedSpeakerName}
667
+ : {}),
668
+ language: typeof v.language === "string" ? v.language : null,
669
+ ...(v.kind === "note" ? {source: v.source === "voice" || v.source === "auto" ? v.source : "ui"} : {}),
670
+ })
671
+ }
672
+ const summaries = (Array.isArray(raw.summaries) ? raw.summaries : [])
673
+ .filter(isSummary)
674
+ .filter((s) => s.text.trim() && Number.isFinite(s.hourStart))
675
+ .map((s) => ({hourStart: s.hourStart, text: s.text, createdAt: s.createdAt}))
676
+ return {entries, summaries}
677
+ }
678
+
570
679
  function isEntry(v: unknown): v is MemoryEntry {
571
680
  if (!v || typeof v !== "object") return false
572
681
  const e = v as Record<string, unknown>
@@ -79,20 +79,26 @@ export function normalizeManifestActions(raw: unknown): DeclaredAction[] {
79
79
  return out
80
80
  }
81
81
 
82
- const WIDGET_SIZES: readonly DeclaredWidgetSize[] = ["XS", "SM", "MD", "LG"]
82
+ const WIDGET_SIZES: readonly DeclaredWidgetSize[] = ["XS", "XSM", "SM", "MD", "LG"]
83
83
  const isWidgetSize = (value: unknown): value is DeclaredWidgetSize => (WIDGET_SIZES as readonly unknown[]).includes(value)
84
84
 
85
85
  /**
86
86
  * Normalize manifest widgets for installed and dev-sideloaded bundles: an
87
87
  * entry without an id, a name or a usable size is dropped, repeats of a
88
88
  * size are folded, and a default size the widget does not offer becomes
89
- * its first size.
89
+ * its first size. Only `expandable: true` makes a widget expandable.
90
90
  */
91
91
  export function normalizeManifestWidgets(raw: unknown): DeclaredWidget[] {
92
92
  if (!Array.isArray(raw)) return []
93
93
  const out: DeclaredWidget[] = []
94
94
  const seen = new Set<string>()
95
- for (const w of raw as Array<{id?: unknown; name?: unknown; sizes?: unknown; defaultSize?: unknown}>) {
95
+ for (const w of raw as Array<{
96
+ id?: unknown
97
+ name?: unknown
98
+ sizes?: unknown
99
+ defaultSize?: unknown
100
+ expandable?: unknown
101
+ }>) {
96
102
  if (!w || typeof w.id !== "string" || !w.id || typeof w.name !== "string" || !w.name || seen.has(w.id)) continue
97
103
  const sizes = Array.isArray(w.sizes) ? Array.from(new Set(w.sizes.filter(isWidgetSize))) : []
98
104
  if (sizes.length === 0) continue
@@ -102,6 +108,7 @@ export function normalizeManifestWidgets(raw: unknown): DeclaredWidget[] {
102
108
  name: w.name,
103
109
  sizes,
104
110
  defaultSize: isWidgetSize(w.defaultSize) && sizes.includes(w.defaultSize) ? w.defaultSize : sizes[0],
111
+ expandable: w.expandable === true,
105
112
  })
106
113
  }
107
114
  return out