@glassly/engine 0.3.5-dev.706 → 0.3.5-dev.713

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 (39) hide show
  1. package/build/engine.d.ts +1 -1
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +1 -1
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/ai.d.ts +3 -3
  6. package/build/facades/ai.d.ts.map +1 -1
  7. package/build/facades/ai.js +3 -3
  8. package/build/facades/ai.js.map +1 -1
  9. package/build/generated/releaseMetadata.js +3 -3
  10. package/build/generated/releaseMetadata.js.map +1 -1
  11. package/build/services/GlassesTouchInput.d.ts +4 -3
  12. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  13. package/build/services/GlassesTouchInput.js +12 -11
  14. package/build/services/GlassesTouchInput.js.map +1 -1
  15. package/build/services/LocalDisplayManager.d.ts +12 -4
  16. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  17. package/build/services/LocalDisplayManager.js +21 -7
  18. package/build/services/LocalDisplayManager.js.map +1 -1
  19. package/build/services/LocalMiniappRuntime.d.ts +3 -0
  20. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  21. package/build/services/LocalMiniappRuntime.js +31 -6
  22. package/build/services/LocalMiniappRuntime.js.map +1 -1
  23. package/build/services/MiniappExitGesture.d.ts +4 -3
  24. package/build/services/MiniappExitGesture.d.ts.map +1 -1
  25. package/build/services/MiniappExitGesture.js +3 -3
  26. package/build/services/MiniappExitGesture.js.map +1 -1
  27. package/build/services/MiniappWidgetMode.d.ts +2 -1
  28. package/build/services/MiniappWidgetMode.d.ts.map +1 -1
  29. package/build/services/MiniappWidgetMode.js +3 -3
  30. package/build/services/MiniappWidgetMode.js.map +1 -1
  31. package/package.json +7 -7
  32. package/src/engine.ts +1 -1
  33. package/src/facades/ai.ts +3 -3
  34. package/src/generated/releaseMetadata.ts +3 -3
  35. package/src/services/GlassesTouchInput.ts +12 -11
  36. package/src/services/LocalDisplayManager.ts +22 -8
  37. package/src/services/LocalMiniappRuntime.ts +28 -6
  38. package/src/services/MiniappExitGesture.ts +5 -4
  39. package/src/services/MiniappWidgetMode.ts +5 -4
@@ -12,7 +12,8 @@ interface ExitGestureState {
12
12
  }
13
13
  export interface ExitGestureDependencies {
14
14
  getState: () => ExitGestureState;
15
- getVisiblePackageName: () => string | null;
15
+ /** The miniapp a double tap leaves right now, or null when the tap is the system's. */
16
+ getTargetPackageName: () => string | null;
16
17
  beginExitNotice: (packageName: string, text: string) => boolean;
17
18
  finishExitNotice: (packageName: string) => void;
18
19
  cancelExitNotice: (packageName: string) => void;
@@ -24,8 +25,8 @@ export declare class MiniappExitGesture {
24
25
  private readonly deps;
25
26
  private pending;
26
27
  constructor(deps: ExitGestureDependencies);
27
- /** Returns true when the system gesture was consumed by a visible miniapp exit. */
28
- tryExitVisibleMiniapp(): boolean;
28
+ /** Returns true when the system gesture was consumed by a miniapp exit. */
29
+ tryExitMiniapp(): boolean;
29
30
  /** Cancel an in-flight handoff when the device-event router shuts down. */
30
31
  reset(): void;
31
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MiniappExitGesture.d.ts","sourceRoot":"","sources":["../../src/services/MiniappExitGesture.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,OAAO,CAAA;AAE1C,UAAU,cAAc;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,UAAU,gBAAgB;IACxB,IAAI,EAAE,cAAc,EAAE,CAAA;IACtB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,gBAAgB,CAAA;IAChC,qBAAqB,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAC1C,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/D,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAC3D,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC;AAED,qEAAqE;AACrE,qBAAa,kBAAkB;IAGV,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFxC,OAAO,CAAC,OAAO,CAAsD;gBAEjC,IAAI,EAAE,uBAAuB;IAEjE,mFAAmF;IAC5E,qBAAqB,IAAI,OAAO;IAmCvC,2EAA2E;IACpE,KAAK,IAAI,IAAI;CAOrB"}
1
+ {"version":3,"file":"MiniappExitGesture.d.ts","sourceRoot":"","sources":["../../src/services/MiniappExitGesture.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,OAAO,CAAA;AAE1C,UAAU,cAAc;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,UAAU,gBAAgB;IACxB,IAAI,EAAE,cAAc,EAAE,CAAA;IACtB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,gBAAgB,CAAA;IAChC,uFAAuF;IACvF,oBAAoB,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACzC,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/D,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAC3D,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC;AAED,qEAAqE;AACrE,qBAAa,kBAAkB;IAGV,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFxC,OAAO,CAAC,OAAO,CAAsD;gBAEjC,IAAI,EAAE,uBAAuB;IAEjE,2EAA2E;IACpE,cAAc,IAAI,OAAO;IAmChC,2EAA2E;IACpE,KAAK,IAAI,IAAI;CAOrB"}
@@ -6,12 +6,12 @@ export class MiniappExitGesture {
6
6
  constructor(deps) {
7
7
  this.deps = deps;
8
8
  }
9
- /** Returns true when the system gesture was consumed by a visible miniapp exit. */
10
- tryExitVisibleMiniapp() {
9
+ /** Returns true when the system gesture was consumed by a miniapp exit. */
10
+ tryExitMiniapp() {
11
11
  // Swallow repeat taps while the notice is already counting down.
12
12
  if (this.pending)
13
13
  return true;
14
- const packageName = this.deps.getVisiblePackageName();
14
+ const packageName = this.deps.getTargetPackageName();
15
15
  if (!packageName)
16
16
  return false;
17
17
  const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName);
@@ -1 +1 @@
1
- {"version":3,"file":"MiniappExitGesture.js","sourceRoot":"","sources":["../../src/services/MiniappExitGesture.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAA;AAyB1C,qEAAqE;AACrE,MAAM,OAAO,kBAAkB;IAGO;IAF5B,OAAO,GAAkD,IAAI,CAAA;IAErE,YAAoC,IAA6B;QAA7B,SAAI,GAAJ,IAAI,CAAyB;IAAG,CAAC;IAErE,mFAAmF;IAC5E,qBAAqB;QAC1B,iEAAiE;QACjE,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QAE7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAA;QACrD,IAAI,CAAC,WAAW;YAAE,OAAO,KAAK,CAAA;QAE9B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,KAAK,WAAW,CAAC,CAAA;QAChG,IAAI,CAAC,GAAG,EAAE,OAAO;YAAE,OAAO,KAAK,CAAA;QAE/B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,WAAW,CAAA;QAC3C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,WAAW,IAAI,GAAG,CAAC;YAAE,OAAO,KAAK,CAAA;QAE7E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAA;YAClC,IAAI,KAAK,CAAC,mBAAmB,KAAK,WAAW;gBAAE,KAAK,CAAC,eAAe,EAAE,CAAA;YAEtE,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,KAAK,WAAW,IAAI,SAAS,CAAC,OAAO,CAAC,CAAA;YAC/G,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAA;YACvE,KAAK,IAAI;iBACN,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;gBACf,OAAO,CAAC,KAAK,CAAC,sCAAsC,WAAW,GAAG,EAAE,KAAK,CAAC,CAAA;YAC5E,CAAC,CAAC;iBACD,OAAO,CAAC,GAAG,EAAE;gBACZ,IAAI,IAAI,CAAC,OAAO,EAAE,WAAW,KAAK,WAAW;oBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;gBAClE,mEAAmE;gBACnE,iDAAiD;gBACjD,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAA;YACzC,CAAC,CAAC,CAAA;QACN,CAAC,EAAE,sBAAsB,CAAC,CAAA;QAE1B,IAAI,CAAC,OAAO,GAAG,EAAC,WAAW,EAAE,OAAO,EAAC,CAAA;QACrC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,2EAA2E;IACpE,KAAK;QACV,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QACzB,MAAM,EAAC,WAAW,EAAE,OAAO,EAAC,GAAG,IAAI,CAAC,OAAO,CAAA;QAC3C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QAC/B,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACzC,CAAC;CACF","sourcesContent":["export const MINIAPP_EXIT_NOTICE_MS = 1500\n\ninterface ExitGestureApp {\n packageName: string\n name: string\n running: boolean\n}\n\ninterface ExitGestureState {\n apps: ExitGestureApp[]\n foregroundedPackage: string | null\n clearForeground: () => void\n stop: (packageName: string) => Promise<void>\n}\n\nexport interface ExitGestureDependencies {\n getState: () => ExitGestureState\n getVisiblePackageName: () => string | null\n beginExitNotice: (packageName: string, text: string) => boolean\n finishExitNotice: (packageName: string) => void\n cancelExitNotice: (packageName: string) => void\n setTimeout: (callback: () => void, delay: number) => number\n clearTimeout: (timerId: number) => void\n}\n\n/** Owns the short double-tap → exit-notice → normal-stop handoff. */\nexport class MiniappExitGesture {\n private pending: {packageName: string; timerId: number} | null = null\n\n public constructor(private readonly deps: ExitGestureDependencies) {}\n\n /** Returns true when the system gesture was consumed by a visible miniapp exit. */\n public tryExitVisibleMiniapp(): boolean {\n // Swallow repeat taps while the notice is already counting down.\n if (this.pending) return true\n\n const packageName = this.deps.getVisiblePackageName()\n if (!packageName) return false\n\n const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)\n if (!app?.running) return false\n\n const name = app.name.trim() || packageName\n if (!this.deps.beginExitNotice(packageName, `Exiting ${name}…`)) return false\n\n const timerId = this.deps.setTimeout(() => {\n const state = this.deps.getState()\n if (state.foregroundedPackage === packageName) state.clearForeground()\n\n const stillRunning = state.apps.some((candidate) => candidate.packageName === packageName && candidate.running)\n const stop = stillRunning ? state.stop(packageName) : Promise.resolve()\n void stop\n .catch((error) => {\n console.error(`MiniappExitGesture: failed to stop ${packageName}:`, error)\n })\n .finally(() => {\n if (this.pending?.packageName === packageName) this.pending = null\n // LocalMiniappRuntime.onUnmount normally finishes the notice. This\n // fallback also handles a failed/no-op teardown.\n this.deps.finishExitNotice(packageName)\n })\n }, MINIAPP_EXIT_NOTICE_MS)\n\n this.pending = {packageName, timerId}\n return true\n }\n\n /** Cancel an in-flight handoff when the device-event router shuts down. */\n public reset(): void {\n if (!this.pending) return\n const {packageName, timerId} = this.pending\n this.pending = null\n this.deps.clearTimeout(timerId)\n this.deps.cancelExitNotice(packageName)\n }\n}\n"]}
1
+ {"version":3,"file":"MiniappExitGesture.js","sourceRoot":"","sources":["../../src/services/MiniappExitGesture.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAA;AA0B1C,qEAAqE;AACrE,MAAM,OAAO,kBAAkB;IAGO;IAF5B,OAAO,GAAkD,IAAI,CAAA;IAErE,YAAoC,IAA6B;QAA7B,SAAI,GAAJ,IAAI,CAAyB;IAAG,CAAC;IAErE,2EAA2E;IACpE,cAAc;QACnB,iEAAiE;QACjE,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QAE7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAA;QACpD,IAAI,CAAC,WAAW;YAAE,OAAO,KAAK,CAAA;QAE9B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,KAAK,WAAW,CAAC,CAAA;QAChG,IAAI,CAAC,GAAG,EAAE,OAAO;YAAE,OAAO,KAAK,CAAA;QAE/B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,WAAW,CAAA;QAC3C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,WAAW,IAAI,GAAG,CAAC;YAAE,OAAO,KAAK,CAAA;QAE7E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAA;YAClC,IAAI,KAAK,CAAC,mBAAmB,KAAK,WAAW;gBAAE,KAAK,CAAC,eAAe,EAAE,CAAA;YAEtE,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,KAAK,WAAW,IAAI,SAAS,CAAC,OAAO,CAAC,CAAA;YAC/G,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAA;YACvE,KAAK,IAAI;iBACN,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;gBACf,OAAO,CAAC,KAAK,CAAC,sCAAsC,WAAW,GAAG,EAAE,KAAK,CAAC,CAAA;YAC5E,CAAC,CAAC;iBACD,OAAO,CAAC,GAAG,EAAE;gBACZ,IAAI,IAAI,CAAC,OAAO,EAAE,WAAW,KAAK,WAAW;oBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;gBAClE,mEAAmE;gBACnE,iDAAiD;gBACjD,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAA;YACzC,CAAC,CAAC,CAAA;QACN,CAAC,EAAE,sBAAsB,CAAC,CAAA;QAE1B,IAAI,CAAC,OAAO,GAAG,EAAC,WAAW,EAAE,OAAO,EAAC,CAAA;QACrC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,2EAA2E;IACpE,KAAK;QACV,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QACzB,MAAM,EAAC,WAAW,EAAE,OAAO,EAAC,GAAG,IAAI,CAAC,OAAO,CAAA;QAC3C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QAC/B,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACzC,CAAC;CACF","sourcesContent":["export const MINIAPP_EXIT_NOTICE_MS = 1500\n\ninterface ExitGestureApp {\n packageName: string\n name: string\n running: boolean\n}\n\ninterface ExitGestureState {\n apps: ExitGestureApp[]\n foregroundedPackage: string | null\n clearForeground: () => void\n stop: (packageName: string) => Promise<void>\n}\n\nexport interface ExitGestureDependencies {\n getState: () => ExitGestureState\n /** The miniapp a double tap leaves right now, or null when the tap is the system's. */\n getTargetPackageName: () => string | null\n beginExitNotice: (packageName: string, text: string) => boolean\n finishExitNotice: (packageName: string) => void\n cancelExitNotice: (packageName: string) => void\n setTimeout: (callback: () => void, delay: number) => number\n clearTimeout: (timerId: number) => void\n}\n\n/** Owns the short double-tap → exit-notice → normal-stop handoff. */\nexport class MiniappExitGesture {\n private pending: {packageName: string; timerId: number} | null = null\n\n public constructor(private readonly deps: ExitGestureDependencies) {}\n\n /** Returns true when the system gesture was consumed by a miniapp exit. */\n public tryExitMiniapp(): boolean {\n // Swallow repeat taps while the notice is already counting down.\n if (this.pending) return true\n\n const packageName = this.deps.getTargetPackageName()\n if (!packageName) return false\n\n const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)\n if (!app?.running) return false\n\n const name = app.name.trim() || packageName\n if (!this.deps.beginExitNotice(packageName, `Exiting ${name}…`)) return false\n\n const timerId = this.deps.setTimeout(() => {\n const state = this.deps.getState()\n if (state.foregroundedPackage === packageName) state.clearForeground()\n\n const stillRunning = state.apps.some((candidate) => candidate.packageName === packageName && candidate.running)\n const stop = stillRunning ? state.stop(packageName) : Promise.resolve()\n void stop\n .catch((error) => {\n console.error(`MiniappExitGesture: failed to stop ${packageName}:`, error)\n })\n .finally(() => {\n if (this.pending?.packageName === packageName) this.pending = null\n // LocalMiniappRuntime.onUnmount normally finishes the notice. This\n // fallback also handles a failed/no-op teardown.\n this.deps.finishExitNotice(packageName)\n })\n }, MINIAPP_EXIT_NOTICE_MS)\n\n this.pending = {packageName, timerId}\n return true\n }\n\n /** Cancel an in-flight handoff when the device-event router shuts down. */\n public reset(): void {\n if (!this.pending) return\n const {packageName, timerId} = this.pending\n this.pending = null\n this.deps.clearTimeout(timerId)\n this.deps.cancelExitNotice(packageName)\n }\n}\n"]}
@@ -9,7 +9,8 @@
9
9
  * The runtime is the writer (placements come and go through it); the apps
10
10
  * store projects `widgetOnly` from here beside `running`.
11
11
  */
12
- type Listener = () => void;
12
+ /** Called with the app whose mode changed and whether it is now widget-only. */
13
+ type Listener = (packageName: string, widgetOnly: boolean) => void;
13
14
  export declare const miniappWidgetMode: {
14
15
  /** Mark (or clear) widget mode. Clearing promotes: the app keeps running as a normal app. */
15
16
  setWidgetOnly(packageName: string, on: boolean): void;
@@ -1 +1 @@
1
- {"version":3,"file":"MiniappWidgetMode.d.ts","sourceRoot":"","sources":["../../src/services/MiniappWidgetMode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAA;AAiB1B,eAAO,MAAM,iBAAiB;IAC5B,6FAA6F;+BAClE,MAAM,MAAM,OAAO,GAAG,IAAI;8BAM3B,MAAM,GAAG,OAAO;IAI1C,oEAAoE;2BAC7C,MAAM,MAAM,OAAO,GAAG,IAAI;0BAK3B,MAAM,GAAG,OAAO;cAI5B,MAAM,EAAE;kBAIJ,QAAQ,GAAG,MAAM,IAAI;CAMpC,CAAA"}
1
+ {"version":3,"file":"MiniappWidgetMode.d.ts","sourceRoot":"","sources":["../../src/services/MiniappWidgetMode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gFAAgF;AAChF,KAAK,QAAQ,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,KAAK,IAAI,CAAA;AAiBlE,eAAO,MAAM,iBAAiB;IAC5B,6FAA6F;+BAClE,MAAM,MAAM,OAAO,GAAG,IAAI;8BAM3B,MAAM,GAAG,OAAO;IAI1C,oEAAoE;2BAC7C,MAAM,MAAM,OAAO,GAAG,IAAI;0BAK3B,MAAM,GAAG,OAAO;cAI5B,MAAM,EAAE;kBAIJ,QAAQ,GAAG,MAAM,IAAI;CAMpC,CAAA"}
@@ -13,10 +13,10 @@ const widgetOnly = new Set();
13
13
  /** Providers with at least one widget placed by some host. */
14
14
  const placed = new Set();
15
15
  const listeners = new Set();
16
- function notify() {
16
+ function notify(packageName, on) {
17
17
  for (const fn of listeners) {
18
18
  try {
19
- fn();
19
+ fn(packageName, on);
20
20
  }
21
21
  catch (e) {
22
22
  console.warn("MiniappWidgetMode: listener threw", e);
@@ -30,7 +30,7 @@ export const miniappWidgetMode = {
30
30
  if (on)
31
31
  widgetOnly.add(packageName);
32
32
  if (changed)
33
- notify();
33
+ notify(packageName, on);
34
34
  },
35
35
  isWidgetOnly(packageName) {
36
36
  return widgetOnly.has(packageName);
@@ -1 +1 @@
1
- {"version":3,"file":"MiniappWidgetMode.js","sourceRoot":"","sources":["../../src/services/MiniappWidgetMode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAA;AACpC,8DAA8D;AAC9D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAA;AAChC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAY,CAAA;AAErC,SAAS,MAAM;IACb,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,EAAE,EAAE,CAAA;QACN,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,CAAC,CAAC,CAAA;QACtD,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,6FAA6F;IAC7F,aAAa,CAAC,WAAmB,EAAE,EAAW;QAC5C,MAAM,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QAClF,IAAI,EAAE;YAAE,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QACnC,IAAI,OAAO;YAAE,MAAM,EAAE,CAAA;IACvB,CAAC;IAED,YAAY,CAAC,WAAmB;QAC9B,OAAO,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACpC,CAAC;IAED,oEAAoE;IACpE,SAAS,CAAC,WAAmB,EAAE,EAAW;QACxC,IAAI,EAAE;YAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;;YAC1B,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACjC,CAAC;IAED,QAAQ,CAAC,WAAmB;QAC1B,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAChC,CAAC;IAED,MAAM;QACJ,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC/B,CAAC;IAED,SAAS,CAAC,EAAY;QACpB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACjB,OAAO,GAAG,EAAE;YACV,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACtB,CAAC,CAAA;IACH,CAAC;CACF,CAAA","sourcesContent":["/**\n * Widget mode: a miniapp whose background context runs only because a host\n * (the dashboard) placed one of its widgets. It is running, but it has no\n * card in the app switcher, draws nothing of its own on the glasses and\n * hears no touch input; the Widgets pill lists it. Opening the app from the\n * home screen promotes it to a normal run; closing a placed app from the\n * tray demotes it back here instead of stopping it.\n *\n * The runtime is the writer (placements come and go through it); the apps\n * store projects `widgetOnly` from here beside `running`.\n */\n\ntype Listener = () => void\n\nconst widgetOnly = new Set<string>()\n/** Providers with at least one widget placed by some host. */\nconst placed = new Set<string>()\nconst listeners = new Set<Listener>()\n\nfunction notify(): void {\n for (const fn of listeners) {\n try {\n fn()\n } catch (e) {\n console.warn(\"MiniappWidgetMode: listener threw\", e)\n }\n }\n}\n\nexport const miniappWidgetMode = {\n /** Mark (or clear) widget mode. Clearing promotes: the app keeps running as a normal app. */\n setWidgetOnly(packageName: string, on: boolean): void {\n const changed = on ? !widgetOnly.has(packageName) : widgetOnly.delete(packageName)\n if (on) widgetOnly.add(packageName)\n if (changed) notify()\n },\n\n isWidgetOnly(packageName: string): boolean {\n return widgetOnly.has(packageName)\n },\n\n /** Record whether any host has a widget of this provider placed. */\n setPlaced(packageName: string, on: boolean): void {\n if (on) placed.add(packageName)\n else placed.delete(packageName)\n },\n\n isPlaced(packageName: string): boolean {\n return placed.has(packageName)\n },\n\n getAll(): string[] {\n return Array.from(widgetOnly)\n },\n\n subscribe(fn: Listener): () => void {\n listeners.add(fn)\n return () => {\n listeners.delete(fn)\n }\n },\n}\n"]}
1
+ {"version":3,"file":"MiniappWidgetMode.js","sourceRoot":"","sources":["../../src/services/MiniappWidgetMode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAA;AACpC,8DAA8D;AAC9D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAA;AAChC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAY,CAAA;AAErC,SAAS,MAAM,CAAC,WAAmB,EAAE,EAAW;IAC9C,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;QACrB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,CAAC,CAAC,CAAA;QACtD,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,6FAA6F;IAC7F,aAAa,CAAC,WAAmB,EAAE,EAAW;QAC5C,MAAM,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QAClF,IAAI,EAAE;YAAE,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QACnC,IAAI,OAAO;YAAE,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;IACtC,CAAC;IAED,YAAY,CAAC,WAAmB;QAC9B,OAAO,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACpC,CAAC;IAED,oEAAoE;IACpE,SAAS,CAAC,WAAmB,EAAE,EAAW;QACxC,IAAI,EAAE;YAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;;YAC1B,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACjC,CAAC;IAED,QAAQ,CAAC,WAAmB;QAC1B,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAChC,CAAC;IAED,MAAM;QACJ,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC/B,CAAC;IAED,SAAS,CAAC,EAAY;QACpB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACjB,OAAO,GAAG,EAAE;YACV,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACtB,CAAC,CAAA;IACH,CAAC;CACF,CAAA","sourcesContent":["/**\n * Widget mode: a miniapp whose background context runs only because a host\n * (the dashboard) placed one of its widgets. It is running, but it has no\n * card in the app switcher, draws nothing of its own on the glasses and\n * hears no touch input; the Widgets pill lists it. Opening the app from the\n * home screen promotes it to a normal run; closing a placed app from the\n * tray demotes it back here instead of stopping it.\n *\n * The runtime is the writer (placements come and go through it); the apps\n * store projects `widgetOnly` from here beside `running`.\n */\n\n/** Called with the app whose mode changed and whether it is now widget-only. */\ntype Listener = (packageName: string, widgetOnly: boolean) => void\n\nconst widgetOnly = new Set<string>()\n/** Providers with at least one widget placed by some host. */\nconst placed = new Set<string>()\nconst listeners = new Set<Listener>()\n\nfunction notify(packageName: string, on: boolean): void {\n for (const fn of listeners) {\n try {\n fn(packageName, on)\n } catch (e) {\n console.warn(\"MiniappWidgetMode: listener threw\", e)\n }\n }\n}\n\nexport const miniappWidgetMode = {\n /** Mark (or clear) widget mode. Clearing promotes: the app keeps running as a normal app. */\n setWidgetOnly(packageName: string, on: boolean): void {\n const changed = on ? !widgetOnly.has(packageName) : widgetOnly.delete(packageName)\n if (on) widgetOnly.add(packageName)\n if (changed) notify(packageName, on)\n },\n\n isWidgetOnly(packageName: string): boolean {\n return widgetOnly.has(packageName)\n },\n\n /** Record whether any host has a widget of this provider placed. */\n setPlaced(packageName: string, on: boolean): void {\n if (on) placed.add(packageName)\n else placed.delete(packageName)\n },\n\n isPlaced(packageName: string): boolean {\n return placed.has(packageName)\n },\n\n getAll(): string[] {\n return Array.from(widgetOnly)\n },\n\n subscribe(fn: Listener): () => void {\n listeners.add(fn)\n return () => {\n listeners.delete(fn)\n }\n },\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.5-dev.706",
3
+ "version": "0.3.5-dev.713",
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-dev.706",
101
- "@glassly/cloud-client": "0.3.5-dev.706",
102
- "@glassly/cloud-protocol": "0.3.5-dev.706",
103
- "@glassly/crust": "0.3.5-dev.706",
104
- "@glassly/miniapp": "0.3.5-dev.706",
100
+ "@glassly/bluetooth-sdk": "0.3.5-dev.713",
101
+ "@glassly/cloud-client": "0.3.5-dev.713",
102
+ "@glassly/cloud-protocol": "0.3.5-dev.713",
103
+ "@glassly/crust": "0.3.5-dev.713",
104
+ "@glassly/miniapp": "0.3.5-dev.713",
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-dev.706",
110
+ "@glassly/llm-providers": "0.3.5-dev.713",
111
111
  "@huggingface/jinja": "^0.5.9",
112
112
  "jsonrepair": "^3.12.0"
113
113
  },
package/src/engine.ts CHANGED
@@ -192,7 +192,7 @@ export const engine = {
192
192
  },
193
193
  glasses,
194
194
  speech,
195
- /** On-device LLM model management (Settings > AI Models). */
195
+ /** On-device LLM model management (Settings > Offline Models). */
196
196
  ai,
197
197
  /** Cloud live-session status + account ops — engine owns the client. */
198
198
  session,
package/src/facades/ai.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * ai facade — `engine.ai.local`: the on-device LLM model that serves
3
3
  * session.ai calls routed to the phone (forceLocal / localFallback). Thin
4
4
  * passthrough over LocalLlmModelManager and LocalLlmProvider so Settings >
5
- * AI Models can list, pick, download, and delete models without reaching
6
- * into engine internals.
5
+ * Offline Models can list, pick, download, and delete models without
6
+ * reaching into engine internals.
7
7
  */
8
8
  import localLlmModelManager, {
9
9
  LocalLlmModelManager,
@@ -17,7 +17,7 @@ export const ai = {
17
17
  local: {
18
18
  /** The catalog, in dropdown order. */
19
19
  models: (): LocalLlmModelConfig[] => localLlmModelManager.models(),
20
- /** The Settings > AI Models pick. */
20
+ /** The Settings > Offline Models pick. */
21
21
  selectedModelId: (): LocalLlmModelId => localLlmModelManager.selectedModelId(),
22
22
  select: (id: LocalLlmModelId): Promise<void> => localLlmModelManager.select(id),
23
23
  isDownloaded: (id?: LocalLlmModelId): Promise<boolean> => localLlmModelManager.isModelAvailable(id),
@@ -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-dev.706",
16
- "releaseSetId": "glassly-0.3.5-dev.706",
17
- "sourceCommit": "a56659581179d47ffa7e11b1533bded63b2ca25d",
15
+ "releaseIdentity": "0.3.5-dev.713",
16
+ "releaseSetId": "glassly-0.3.5-dev.713",
17
+ "sourceCommit": "2edfba21f7f09001cd71e530720fd93261eacd42",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -17,7 +17,7 @@ import {pagesEligible} from "./pages/pageEligibility"
17
17
 
18
18
  const miniappExitGesture = new MiniappExitGesture({
19
19
  getState: () => useAppStatusStore.getState(),
20
- getVisiblePackageName: () => localDisplayManager.getVisibleMiniappPackageName(),
20
+ getTargetPackageName: () => localDisplayManager.getMiniappToLeave(),
21
21
  beginExitNotice: (packageName, text) => localDisplayManager.beginMiniappExit(packageName, text),
22
22
  finishExitNotice: (packageName) => localDisplayManager.finishMiniappExit(packageName),
23
23
  cancelExitNotice: (packageName) => localDisplayManager.cancelMiniappExit(packageName),
@@ -47,9 +47,10 @@ function shouldForwardScroll(event: TouchEvent): boolean {
47
47
  * Route both real glasses input and phone mirror controls through the same
48
48
  * policy. A claimed gesture goes to the app holding the claim, alone; an
49
49
  * unclaimed one goes to every subscriber and then the engine's default. The
50
- * one rule ahead of the claims: an unclaimed double tap on a miniapp whose
51
- * frame is visibly on the glasses leaves that app, whoever else claimed the
52
- * gesture — the dashboard's claim is for the empty screen. Head position
50
+ * one rule ahead of the claims: an unclaimed double tap leaves the miniapp
51
+ * the wearer is in — the one visibly on the glasses, else the running
52
+ * standard app on an empty screen — whoever else claimed the gesture; the
53
+ * dashboard's claim is for the screen with no app in it. Head position
53
54
  * (head_up / head_down) is a gesture like any other: the dashboard claims it
54
55
  * while it runs, and an unclaimed head_up on glasses that defer the dashboard
55
56
  * to the phone opens the native one, as it would with no app at all.
@@ -63,16 +64,16 @@ export function dispatchTouchEvent(event: TouchEvent): void {
63
64
  const osHeadUp = gesture === "head_up"
64
65
 
65
66
  if (osDoubleTap) {
66
- // A visible main miniapp opts out of OS exit by registering double tap (the
67
+ // The app being left opts out of OS exit by registering double tap (the
67
68
  // terminal's back-to-menu). The latest claimant still receives it. Otherwise
68
69
  // the tap means "leave the app". The controller shows the bounded OS
69
- // notice first, then uses the normal
70
- // store stop path (and closes its phone overlay).
71
- const visible = localDisplayManager.getVisibleMiniappPackageName()
70
+ // notice first, then uses the normal store stop path (and closes its
71
+ // phone overlay).
72
+ const leaving = localDisplayManager.getMiniappToLeave()
72
73
  if (
73
- visible !== null &&
74
- !localMiniappRuntime.hasGestureClaim(visible, gesture) &&
75
- miniappExitGesture.tryExitVisibleMiniapp()
74
+ leaving !== null &&
75
+ !localMiniappRuntime.hasGestureClaim(leaving, gesture) &&
76
+ miniappExitGesture.tryExitMiniapp()
76
77
  ) {
77
78
  gestureDotsService.noteGesture(gesture, true, event.index, event.controllerId)
78
79
  return
@@ -165,7 +165,7 @@ class LocalDisplayManager {
165
165
  private overlayOwner: string | null = null
166
166
 
167
167
  /** Miniapp whose visible frame is temporarily covered by an OS exit notice. */
168
- private exitingApp: {packageName: string; coveredDisplay: ActiveDisplay} | null = null
168
+ private exitingApp: {packageName: string; coveredDisplay: ActiveDisplay | null} | null = null
169
169
 
170
170
  private expiryTimerId: number | null = null
171
171
 
@@ -309,16 +309,30 @@ class LocalDisplayManager {
309
309
  }
310
310
 
311
311
  /**
312
- * Cover the currently visible miniapp with a persistent system exit notice.
313
- * While it is up, miniapp display traffic is held off so a streaming app
314
- * cannot immediately overwrite the notice. The caller completes the normal
315
- * stop flow and then calls finishMiniappExit(); onUnmount does the same as a
312
+ * The miniapp an unclaimed double tap leaves: the one visibly drawing on
313
+ * the main view, else the core (running standard) app on an empty or
314
+ * system-drawn screen — an app that has drawn nothing yet, or whose frame
315
+ * expired, is still the one the wearer is in. Nothing while a page covers
316
+ * the main view: the page's claims own the tap.
317
+ */
318
+ public getMiniappToLeave(): string | null {
319
+ if (this.overlayOwner !== null) return null
320
+ return this.getVisibleMiniappPackageName() ?? this.coreApp
321
+ }
322
+
323
+ /**
324
+ * Cover the leaving miniapp with a persistent system exit notice. While it
325
+ * is up, miniapp display traffic is held off so a streaming app cannot
326
+ * immediately overwrite the notice. The caller completes the normal stop
327
+ * flow and then calls finishMiniappExit(); onUnmount does the same as a
316
328
  * safety net.
317
329
  */
318
330
  public beginMiniappExit(packageName: string, text: string): boolean {
319
331
  if (this.exitingApp !== null) return this.exitingApp.packageName === packageName
320
- const coveredDisplay = this.currentDisplay
321
- if (!coveredDisplay || this.getVisibleMiniappPackageName() !== packageName) return false
332
+ if (this.getMiniappToLeave() !== packageName) return false
333
+ // Only the app's own visible frame comes back on cancel; a system frame
334
+ // or an empty screen does not.
335
+ const coveredDisplay = this.getVisibleMiniappPackageName() === packageName ? this.currentDisplay : null
322
336
 
323
337
  console.log(`${LOG_TAG}: beginMiniappExit(${packageName})`)
324
338
  this.exitingApp = {packageName, coveredDisplay}
@@ -349,7 +363,7 @@ class LocalDisplayManager {
349
363
  this.exitingApp = null
350
364
 
351
365
  if (this.currentDisplay?.packageName !== SYSTEM_NOTICE_PKG) return
352
- if (covered.expiresAt === null || covered.expiresAt > this.now()) {
366
+ if (covered && (covered.expiresAt === null || covered.expiresAt > this.now())) {
353
367
  this.restoreDisplay(covered)
354
368
  } else {
355
369
  this.currentDisplay = null
@@ -641,6 +641,10 @@ class LocalMiniappRuntime {
641
641
  private connectedApps: Map<string, ConnectedMiniapp> = new Map()
642
642
  /** Claimed gestures (CLAIM_GESTURES): a stack per gesture, the most recent claimer holding it. */
643
643
  private gestureClaims: GestureClaimState = emptyGestureClaims()
644
+ // The last main-view frame each app asked to render. Widget mode refuses
645
+ // frames and demotion clears them, and an app that has drawn once may not
646
+ // draw again on its own, so promotion replays this.
647
+ private retainedRenders = new Map<string, Record<string, unknown>>()
644
648
 
645
649
  /**
646
650
  * Packages that have completed their CONNECT handshake (sent
@@ -705,7 +709,13 @@ class LocalMiniappRuntime {
705
709
  private localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`
706
710
  private usedTokens = new Set<string>()
707
711
 
708
- private constructor() {}
712
+ private constructor() {
713
+ miniappWidgetMode.subscribe((packageName, widgetOnly) => {
714
+ // Promotion happens inside the apps store's start(); the core-app slot
715
+ // settles with that store write, so replay once it has.
716
+ if (!widgetOnly) queueMicrotask(() => this.replayRetainedRender(packageName))
717
+ })
718
+ }
709
719
 
710
720
  /** Snapshot of miniapps actively subscribed to hardware button events. */
711
721
  public getButtonPressSubscribers(): string[] {
@@ -1244,6 +1254,7 @@ class LocalMiniappRuntime {
1244
1254
  }
1245
1255
  // Its gesture claims go back to whoever claimed them before.
1246
1256
  this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName))
1257
+ this.retainedRenders.delete(packageName)
1247
1258
 
1248
1259
  // Warm-up is a request-owned camera lease. Cancel it even if the app record was already
1249
1260
  // removed so a close racing the camera-open promise cannot leave the sensor running.
@@ -2489,11 +2500,6 @@ class LocalMiniappRuntime {
2489
2500
  this.sendResult(packageName, requestId, true, {status: "blocked", reason: "app stopped"})
2490
2501
  return
2491
2502
  }
2492
- // A widget-only app draws through its widget alone.
2493
- if (miniappWidgetMode.isWidgetOnly(packageName)) {
2494
- this.sendResult(packageName, requestId, true, {status: "blocked", reason: "widget mode"})
2495
- return
2496
- }
2497
2503
  if (!Array.isArray(payload.elements)) {
2498
2504
  this.sendResult(packageName, requestId, false, undefined, {
2499
2505
  code: MiniappErrorCode.INTERNAL,
@@ -2501,6 +2507,15 @@ class LocalMiniappRuntime {
2501
2507
  })
2502
2508
  return
2503
2509
  }
2510
+ if (payload.view !== "dashboard") {
2511
+ if (payload.elements.length === 0) this.retainedRenders.delete(packageName)
2512
+ else this.retainedRenders.set(packageName, payload)
2513
+ }
2514
+ // A widget-only app draws through its widget alone; the frame waits for promotion.
2515
+ if (miniappWidgetMode.isWidgetOnly(packageName)) {
2516
+ this.sendResult(packageName, requestId, true, {status: "blocked", reason: "widget mode"})
2517
+ return
2518
+ }
2504
2519
  // The dashboard view is the overlay app's alone; anyone else's frame
2505
2520
  // is dropped, reported rather than silently lost.
2506
2521
  if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
@@ -2530,6 +2545,13 @@ class LocalMiniappRuntime {
2530
2545
  }
2531
2546
  }
2532
2547
 
2548
+ /** Put a promoted app's last main-view frame on the glasses; widget mode had refused it. */
2549
+ private replayRetainedRender(packageName: string): void {
2550
+ const payload = this.retainedRenders.get(packageName)
2551
+ if (!payload || !this.connectedApps.has(packageName) || miniappWidgetMode.isWidgetOnly(packageName)) return
2552
+ this.handleRender(packageName, payload)
2553
+ }
2554
+
2533
2555
  /**
2534
2556
  * display.showView: the active dashboard takes or releases the overlay
2535
2557
  * slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
@@ -15,7 +15,8 @@ interface ExitGestureState {
15
15
 
16
16
  export interface ExitGestureDependencies {
17
17
  getState: () => ExitGestureState
18
- getVisiblePackageName: () => string | null
18
+ /** The miniapp a double tap leaves right now, or null when the tap is the system's. */
19
+ getTargetPackageName: () => string | null
19
20
  beginExitNotice: (packageName: string, text: string) => boolean
20
21
  finishExitNotice: (packageName: string) => void
21
22
  cancelExitNotice: (packageName: string) => void
@@ -29,12 +30,12 @@ export class MiniappExitGesture {
29
30
 
30
31
  public constructor(private readonly deps: ExitGestureDependencies) {}
31
32
 
32
- /** Returns true when the system gesture was consumed by a visible miniapp exit. */
33
- public tryExitVisibleMiniapp(): boolean {
33
+ /** Returns true when the system gesture was consumed by a miniapp exit. */
34
+ public tryExitMiniapp(): boolean {
34
35
  // Swallow repeat taps while the notice is already counting down.
35
36
  if (this.pending) return true
36
37
 
37
- const packageName = this.deps.getVisiblePackageName()
38
+ const packageName = this.deps.getTargetPackageName()
38
39
  if (!packageName) return false
39
40
 
40
41
  const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)
@@ -10,17 +10,18 @@
10
10
  * store projects `widgetOnly` from here beside `running`.
11
11
  */
12
12
 
13
- type Listener = () => void
13
+ /** Called with the app whose mode changed and whether it is now widget-only. */
14
+ type Listener = (packageName: string, widgetOnly: boolean) => void
14
15
 
15
16
  const widgetOnly = new Set<string>()
16
17
  /** Providers with at least one widget placed by some host. */
17
18
  const placed = new Set<string>()
18
19
  const listeners = new Set<Listener>()
19
20
 
20
- function notify(): void {
21
+ function notify(packageName: string, on: boolean): void {
21
22
  for (const fn of listeners) {
22
23
  try {
23
- fn()
24
+ fn(packageName, on)
24
25
  } catch (e) {
25
26
  console.warn("MiniappWidgetMode: listener threw", e)
26
27
  }
@@ -32,7 +33,7 @@ export const miniappWidgetMode = {
32
33
  setWidgetOnly(packageName: string, on: boolean): void {
33
34
  const changed = on ? !widgetOnly.has(packageName) : widgetOnly.delete(packageName)
34
35
  if (on) widgetOnly.add(packageName)
35
- if (changed) notify()
36
+ if (changed) notify(packageName, on)
36
37
  },
37
38
 
38
39
  isWidgetOnly(packageName: string): boolean {