@glassly/engine 0.3.5-beta.710 → 0.3.5-beta.734
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.
- package/build/engine.d.ts +1 -1
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +1 -1
- package/build/engine.js.map +1 -1
- package/build/facades/ai.d.ts +3 -3
- package/build/facades/ai.d.ts.map +1 -1
- package/build/facades/ai.js +3 -3
- package/build/facades/ai.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts +4 -3
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +12 -11
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +12 -4
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +21 -7
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +3 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +40 -9
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappExitGesture.d.ts +6 -3
- package/build/services/MiniappExitGesture.d.ts.map +1 -1
- package/build/services/MiniappExitGesture.js +4 -4
- package/build/services/MiniappExitGesture.js.map +1 -1
- package/build/services/MiniappWidgetMode.d.ts +6 -4
- package/build/services/MiniappWidgetMode.d.ts.map +1 -1
- package/build/services/MiniappWidgetMode.js +7 -6
- package/build/services/MiniappWidgetMode.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +13 -11
- package/build/stores/apps.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +1 -1
- package/src/facades/ai.ts +3 -3
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GlassesTouchInput.ts +12 -11
- package/src/services/LocalDisplayManager.ts +22 -8
- package/src/services/LocalMiniappRuntime.ts +37 -9
- package/src/services/MiniappExitGesture.ts +8 -5
- package/src/services/MiniappWidgetMode.ts +9 -7
- package/src/stores/apps.ts +12 -10
|
@@ -17,7 +17,7 @@ import {pagesEligible} from "./pages/pageEligibility"
|
|
|
17
17
|
|
|
18
18
|
const miniappExitGesture = new MiniappExitGesture({
|
|
19
19
|
getState: () => useAppStatusStore.getState(),
|
|
20
|
-
|
|
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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
71
|
-
const
|
|
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
|
-
|
|
74
|
-
!localMiniappRuntime.hasGestureClaim(
|
|
75
|
-
miniappExitGesture.
|
|
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
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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
|
-
|
|
321
|
-
|
|
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[] {
|
|
@@ -1122,7 +1132,11 @@ class LocalMiniappRuntime {
|
|
|
1122
1132
|
// window ends early once this app pushes its first display (see
|
|
1123
1133
|
// LocalDisplayManager.request), or after ~1.5s. Fires once per spawn —
|
|
1124
1134
|
// including a crash-respawn, since each spawn is a fresh "app is starting".
|
|
1125
|
-
|
|
1135
|
+
// A spawn for a dashboard widget is not the wearer starting the app: it draws
|
|
1136
|
+
// nothing of its own, so it shows no boot text either.
|
|
1137
|
+
if (!miniappWidgetMode.isWidgetOnly(packageName)) {
|
|
1138
|
+
localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName)
|
|
1139
|
+
}
|
|
1126
1140
|
}
|
|
1127
1141
|
|
|
1128
1142
|
/**
|
|
@@ -1244,6 +1258,7 @@ class LocalMiniappRuntime {
|
|
|
1244
1258
|
}
|
|
1245
1259
|
// Its gesture claims go back to whoever claimed them before.
|
|
1246
1260
|
this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName))
|
|
1261
|
+
this.retainedRenders.delete(packageName)
|
|
1247
1262
|
|
|
1248
1263
|
// Warm-up is a request-owned camera lease. Cancel it even if the app record was already
|
|
1249
1264
|
// removed so a close racing the camera-open promise cannot leave the sensor running.
|
|
@@ -2489,11 +2504,6 @@ class LocalMiniappRuntime {
|
|
|
2489
2504
|
this.sendResult(packageName, requestId, true, {status: "blocked", reason: "app stopped"})
|
|
2490
2505
|
return
|
|
2491
2506
|
}
|
|
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
2507
|
if (!Array.isArray(payload.elements)) {
|
|
2498
2508
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
2499
2509
|
code: MiniappErrorCode.INTERNAL,
|
|
@@ -2501,6 +2511,15 @@ class LocalMiniappRuntime {
|
|
|
2501
2511
|
})
|
|
2502
2512
|
return
|
|
2503
2513
|
}
|
|
2514
|
+
if (payload.view !== "dashboard") {
|
|
2515
|
+
if (payload.elements.length === 0) this.retainedRenders.delete(packageName)
|
|
2516
|
+
else this.retainedRenders.set(packageName, payload)
|
|
2517
|
+
}
|
|
2518
|
+
// A widget-only app draws through its widget alone; the frame waits for promotion.
|
|
2519
|
+
if (miniappWidgetMode.isWidgetOnly(packageName)) {
|
|
2520
|
+
this.sendResult(packageName, requestId, true, {status: "blocked", reason: "widget mode"})
|
|
2521
|
+
return
|
|
2522
|
+
}
|
|
2504
2523
|
// The dashboard view is the overlay app's alone; anyone else's frame
|
|
2505
2524
|
// is dropped, reported rather than silently lost.
|
|
2506
2525
|
if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
|
|
@@ -2530,6 +2549,13 @@ class LocalMiniappRuntime {
|
|
|
2530
2549
|
}
|
|
2531
2550
|
}
|
|
2532
2551
|
|
|
2552
|
+
/** Put a promoted app's last main-view frame on the glasses; widget mode had refused it. */
|
|
2553
|
+
private replayRetainedRender(packageName: string): void {
|
|
2554
|
+
const payload = this.retainedRenders.get(packageName)
|
|
2555
|
+
if (!payload || !this.connectedApps.has(packageName) || miniappWidgetMode.isWidgetOnly(packageName)) return
|
|
2556
|
+
this.handleRender(packageName, payload)
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2533
2559
|
/**
|
|
2534
2560
|
* display.showView: the active dashboard takes or releases the overlay
|
|
2535
2561
|
* slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
|
|
@@ -6381,10 +6407,12 @@ class LocalMiniappRuntime {
|
|
|
6381
6407
|
}
|
|
6382
6408
|
const provider = app.packageName
|
|
6383
6409
|
// Record the placement before the wake so the spawn lands in widget mode
|
|
6384
|
-
// and an unplace racing it cannot stop the provider early.
|
|
6410
|
+
// and an unplace racing it cannot stop the provider early. A provider the
|
|
6411
|
+
// store already runs (a wearer's launch still spawning, say) stays a
|
|
6412
|
+
// normal run; the registry alone lags that launch.
|
|
6385
6413
|
this.widgetPlacements = placeWidget(this.widgetPlacements, {hostPackageName, providerPackageName: provider, widgetId, box})
|
|
6386
6414
|
miniappWidgetMode.setPlaced(provider, true)
|
|
6387
|
-
if (!miniappRunningRegistry.has(provider)) miniappWidgetMode.setWidgetOnly(provider, true)
|
|
6415
|
+
if (!app.running && !miniappRunningRegistry.has(provider)) miniappWidgetMode.setWidgetOnly(provider, true)
|
|
6388
6416
|
try {
|
|
6389
6417
|
await this.wakeInteropApp(provider)
|
|
6390
6418
|
} catch (e) {
|
|
@@ -4,6 +4,8 @@ interface ExitGestureApp {
|
|
|
4
4
|
packageName: string
|
|
5
5
|
name: string
|
|
6
6
|
running: boolean
|
|
7
|
+
/** Running only for a placed widget: the wearer is not in it, so there is nothing to leave. */
|
|
8
|
+
widgetOnly?: boolean
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
interface ExitGestureState {
|
|
@@ -15,7 +17,8 @@ interface ExitGestureState {
|
|
|
15
17
|
|
|
16
18
|
export interface ExitGestureDependencies {
|
|
17
19
|
getState: () => ExitGestureState
|
|
18
|
-
|
|
20
|
+
/** The miniapp a double tap leaves right now, or null when the tap is the system's. */
|
|
21
|
+
getTargetPackageName: () => string | null
|
|
19
22
|
beginExitNotice: (packageName: string, text: string) => boolean
|
|
20
23
|
finishExitNotice: (packageName: string) => void
|
|
21
24
|
cancelExitNotice: (packageName: string) => void
|
|
@@ -29,16 +32,16 @@ export class MiniappExitGesture {
|
|
|
29
32
|
|
|
30
33
|
public constructor(private readonly deps: ExitGestureDependencies) {}
|
|
31
34
|
|
|
32
|
-
/** Returns true when the system gesture was consumed by a
|
|
33
|
-
public
|
|
35
|
+
/** Returns true when the system gesture was consumed by a miniapp exit. */
|
|
36
|
+
public tryExitMiniapp(): boolean {
|
|
34
37
|
// Swallow repeat taps while the notice is already counting down.
|
|
35
38
|
if (this.pending) return true
|
|
36
39
|
|
|
37
|
-
const packageName = this.deps.
|
|
40
|
+
const packageName = this.deps.getTargetPackageName()
|
|
38
41
|
if (!packageName) return false
|
|
39
42
|
|
|
40
43
|
const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)
|
|
41
|
-
if (!app?.running) return false
|
|
44
|
+
if (!app?.running || app.widgetOnly) return false
|
|
42
45
|
|
|
43
46
|
const name = app.name.trim() || packageName
|
|
44
47
|
if (!this.deps.beginExitNotice(packageName, `Exiting ${name}…`)) return false
|
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
* Widget mode: a miniapp whose background context runs only because a host
|
|
3
3
|
* (the dashboard) placed one of its widgets. It is running, but it has no
|
|
4
4
|
* card in the app switcher, draws nothing of its own on the glasses and
|
|
5
|
-
* hears no touch input; the Widgets pill lists it.
|
|
6
|
-
*
|
|
7
|
-
* tray demotes it back here instead of
|
|
5
|
+
* hears no touch input; the Widgets pill lists it. Foregrounding the app on
|
|
6
|
+
* the phone promotes it to a normal run (the store's start/setForeground);
|
|
7
|
+
* closing a placed app from the tray demotes it back here instead of
|
|
8
|
+
* stopping it.
|
|
8
9
|
*
|
|
9
10
|
* The runtime is the writer (placements come and go through it); the apps
|
|
10
11
|
* store projects `widgetOnly` from here beside `running`.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/** Called with the app whose mode changed and whether it is now widget-only. */
|
|
15
|
+
type Listener = (packageName: string, widgetOnly: boolean) => void
|
|
14
16
|
|
|
15
17
|
const widgetOnly = new Set<string>()
|
|
16
18
|
/** Providers with at least one widget placed by some host. */
|
|
17
19
|
const placed = new Set<string>()
|
|
18
20
|
const listeners = new Set<Listener>()
|
|
19
21
|
|
|
20
|
-
function notify(): void {
|
|
22
|
+
function notify(packageName: string, on: boolean): void {
|
|
21
23
|
for (const fn of listeners) {
|
|
22
24
|
try {
|
|
23
|
-
fn()
|
|
25
|
+
fn(packageName, on)
|
|
24
26
|
} catch (e) {
|
|
25
27
|
console.warn("MiniappWidgetMode: listener threw", e)
|
|
26
28
|
}
|
|
@@ -32,7 +34,7 @@ export const miniappWidgetMode = {
|
|
|
32
34
|
setWidgetOnly(packageName: string, on: boolean): void {
|
|
33
35
|
const changed = on ? !widgetOnly.has(packageName) : widgetOnly.delete(packageName)
|
|
34
36
|
if (on) widgetOnly.add(packageName)
|
|
35
|
-
if (changed) notify()
|
|
37
|
+
if (changed) notify(packageName, on)
|
|
36
38
|
},
|
|
37
39
|
|
|
38
40
|
isWidgetOnly(packageName: string): boolean {
|
package/src/stores/apps.ts
CHANGED
|
@@ -256,6 +256,7 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
|
|
|
256
256
|
// across both passes.
|
|
257
257
|
foregrounded: app.packageName === previousState.foregroundedPackage,
|
|
258
258
|
paused: app.local && app.running && isMiniappsPaused(),
|
|
259
|
+
widgetOnly: !!app.local && app.running && miniappWidgetMode.isWidgetOnly(app.packageName),
|
|
259
260
|
}
|
|
260
261
|
const prev = previousByPackage.get(app.packageName)
|
|
261
262
|
return prev && shallowEqualApp(prev, next) ? prev : next
|
|
@@ -362,6 +363,11 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
362
363
|
return false
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
// An app running only for its widget is promoted before the host hook:
|
|
367
|
+
// the hook may foreground it, and setForeground sends a widget-only app
|
|
368
|
+
// back here.
|
|
369
|
+
if (app.widgetOnly) miniappWidgetMode.setWidgetOnly(packageName, false)
|
|
370
|
+
|
|
365
371
|
// Host navigation / open-animation seam. Runs on EVERY accepted open —
|
|
366
372
|
// including re-opening an already-running app — so the animation always
|
|
367
373
|
// plays (BuiltInMiniappCatalog.navigateForApp: overlay apps setForeground
|
|
@@ -374,13 +380,12 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
374
380
|
|
|
375
381
|
// If the tapped app is already running, this is a re-open: the nav/foreground
|
|
376
382
|
// above already drove the animation, and there's nothing to spawn. Short-
|
|
377
|
-
// circuit before the spawn pipeline.
|
|
378
|
-
//
|
|
383
|
+
// circuit before the spawn pipeline. A promoted app keeps its context but
|
|
384
|
+
// still takes the foreground below.
|
|
379
385
|
if (app.running && !app.widgetOnly) {
|
|
380
386
|
saveLastOpenTime(packageName)
|
|
381
387
|
return true
|
|
382
388
|
}
|
|
383
|
-
if (app.widgetOnly) miniappWidgetMode.setWidgetOnly(packageName, false)
|
|
384
389
|
|
|
385
390
|
// Only-one rules: one standard app in the foreground, one dashboard
|
|
386
391
|
// drawing the overlay. Starting another stops the one running. Widget-only
|
|
@@ -495,13 +500,10 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
495
500
|
apps: s.apps.map((a) => ({...a, foregrounded: a.packageName === packageName})),
|
|
496
501
|
}))
|
|
497
502
|
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
// if (!app.running) {
|
|
503
|
-
// get().start(app)
|
|
504
|
-
// }
|
|
503
|
+
// The wearer is in this app now: one running only for its widget becomes
|
|
504
|
+
// a normal run. start() promotes it, stops the standard peer and keeps the
|
|
505
|
+
// context. Fire-and-forget after the flag flip so it doesn't block paint.
|
|
506
|
+
if (app.running && app.widgetOnly) void get().start(app, {skipNavigation: true})
|
|
505
507
|
},
|
|
506
508
|
|
|
507
509
|
clearForeground: () => {
|