@glassly/engine 0.3.7 → 0.3.8-dev.1000
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/README.md +4 -0
- package/build/engine.d.ts +10 -2
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -4
- package/build/engine.js.map +1 -1
- package/build/facades/ai.d.ts +3 -0
- package/build/facades/ai.d.ts.map +1 -1
- package/build/facades/ai.js +3 -0
- package/build/facades/ai.js.map +1 -1
- package/build/facades/backup.d.ts +14 -0
- package/build/facades/backup.d.ts.map +1 -0
- package/build/facades/backup.js +22 -0
- package/build/facades/backup.js.map +1 -0
- package/build/facades/glasses.d.ts +4 -4
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +4 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppRegistry.d.ts +2 -1
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +38 -8
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AppStateBackup.d.ts +98 -0
- package/build/services/AppStateBackup.d.ts.map +1 -0
- package/build/services/AppStateBackup.js +224 -0
- package/build/services/AppStateBackup.js.map +1 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +5 -12
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudClientService.d.ts +1 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +34 -2
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/DisplayDiagnostics.d.ts.map +1 -1
- package/build/services/DisplayDiagnostics.js +4 -2
- package/build/services/DisplayDiagnostics.js.map +1 -1
- package/build/services/DisplayProcessor.d.ts +2 -0
- package/build/services/DisplayProcessor.d.ts.map +1 -1
- package/build/services/DisplayProcessor.js +15 -1
- package/build/services/DisplayProcessor.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +4 -2
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/JSCrashloopReportService.d.ts.map +1 -1
- package/build/services/JSCrashloopReportService.js +5 -0
- package/build/services/JSCrashloopReportService.js.map +1 -1
- package/build/services/JSRouter.d.ts +11 -3
- package/build/services/JSRouter.d.ts.map +1 -1
- package/build/services/JSRouter.js +36 -6
- package/build/services/JSRouter.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +5 -2
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +13 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +61 -22
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.js +2 -0
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
- package/build/services/LocalTranslationService.d.ts +2 -0
- package/build/services/LocalTranslationService.d.ts.map +1 -1
- package/build/services/LocalTranslationService.js +22 -0
- package/build/services/LocalTranslationService.js.map +1 -1
- package/build/services/MiniappEngine.d.ts +9 -0
- package/build/services/MiniappEngine.d.ts.map +1 -1
- package/build/services/MiniappEngine.js +60 -27
- package/build/services/MiniappEngine.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts +2 -0
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +6 -0
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/NavigationService.d.ts +1 -0
- package/build/services/NavigationService.d.ts.map +1 -1
- package/build/services/NavigationService.js +20 -6
- package/build/services/NavigationService.js.map +1 -1
- package/build/services/OfflineActivity.d.ts +54 -0
- package/build/services/OfflineActivity.d.ts.map +1 -0
- package/build/services/OfflineActivity.js +86 -0
- package/build/services/OfflineActivity.js.map +1 -0
- package/build/services/PhoneCalendarService.d.ts.map +1 -1
- package/build/services/PhoneCalendarService.js +8 -0
- package/build/services/PhoneCalendarService.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +7 -3
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/STTModelManager.d.ts +1 -0
- package/build/services/STTModelManager.d.ts.map +1 -1
- package/build/services/STTModelManager.js +6 -0
- package/build/services/STTModelManager.js.map +1 -1
- package/build/services/SceneRenderer.js +1 -1
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/appStateBackupFile.d.ts +32 -0
- package/build/services/appStateBackupFile.d.ts.map +1 -0
- package/build/services/appStateBackupFile.js +135 -0
- package/build/services/appStateBackupFile.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +12 -3
- package/build/services/controllers.js.map +1 -1
- package/build/services/effectiveCapabilities.js +6 -1
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/services/pages/pageEligibility.d.ts.map +1 -1
- package/build/services/pages/pageEligibility.js +8 -1
- package/build/services/pages/pageEligibility.js.map +1 -1
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +1 -0
- package/build/services/settingsBridge.js.map +1 -1
- package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
- package/build/services/watch/WatchLink.d.ts.map +1 -0
- package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
- package/build/services/watch/WatchLink.js.map +1 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +22 -10
- package/build/stores/apps.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +63 -6
- package/build/stores/settings.js.map +1 -1
- package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g1.js +3 -0
- package/build/types/capabilities/even-realities-g1.js.map +1 -1
- package/build/types/capabilities/memomind-one.d.ts +3 -0
- package/build/types/capabilities/memomind-one.d.ts.map +1 -0
- package/build/types/capabilities/memomind-one.js +42 -0
- package/build/types/capabilities/memomind-one.js.map +1 -0
- package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
- package/build/types/capabilities/rayneo-io.js +3 -2
- package/build/types/capabilities/rayneo-io.js.map +1 -1
- package/build/types/enums.d.ts +7 -2
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +5 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +3 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +3 -1
- package/build/types/hardware.js.map +1 -1
- package/build/utils/display/index.d.ts +2 -1
- package/build/utils/display/index.d.ts.map +1 -1
- package/build/utils/display/index.js +3 -2
- package/build/utils/display/index.js.map +1 -1
- package/build/utils/display/profiles/index.d.ts +2 -1
- package/build/utils/display/profiles/index.d.ts.map +1 -1
- package/build/utils/display/profiles/index.js +3 -2
- package/build/utils/display/profiles/index.js.map +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
- package/build/utils/display/profiles/rayneo-io.js +1 -1
- package/build/utils/display/profiles/rayneo-io.js.map +1 -1
- package/build/utils/display/scene/index.d.ts +1 -1
- package/build/utils/display/scene/index.d.ts.map +1 -1
- package/build/utils/display/scene/index.js +1 -1
- package/build/utils/display/scene/index.js.map +1 -1
- package/build/utils/display/scene/process.d.ts +6 -0
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +22 -7
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/miniappGlobals.d.ts +13 -17
- package/build/utils/miniappGlobals.d.ts.map +1 -1
- package/build/utils/miniappGlobals.js +24 -56
- package/build/utils/miniappGlobals.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +7 -4
- package/src/facades/ai.ts +4 -0
- package/src/facades/backup.ts +28 -0
- package/src/facades/glasses.ts +5 -5
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +9 -0
- package/src/services/AppRegistry.ts +36 -9
- package/src/services/AppStateBackup.ts +283 -0
- package/src/services/BlobStore.ts +5 -11
- package/src/services/CloudClientService.ts +35 -4
- package/src/services/DisplayDiagnostics.ts +4 -2
- package/src/services/DisplayProcessor.ts +20 -8
- package/src/services/GlassesTouchInput.ts +6 -2
- package/src/services/JSCrashloopReportService.ts +5 -0
- package/src/services/JSRouter.ts +37 -6
- package/src/services/LocalDisplayManager.ts +5 -2
- package/src/services/LocalMiniappRuntime.ts +61 -22
- package/src/services/LocalSttFallbackCoordinator.ts +4 -0
- package/src/services/LocalTranslationService.ts +20 -0
- package/src/services/MiniappEngine.ts +62 -27
- package/src/services/MiniappLauncher.ts +7 -0
- package/src/services/NavigationService.ts +28 -8
- package/src/services/OfflineActivity.ts +131 -0
- package/src/services/PhoneCalendarService.ts +7 -0
- package/src/services/PhoneNotificationsSync.ts +8 -3
- package/src/services/STTModelManager.ts +6 -0
- package/src/services/SceneRenderer.ts +1 -1
- package/src/services/appStateBackupFile.ts +171 -0
- package/src/services/controllers.ts +13 -3
- package/src/services/effectiveCapabilities.ts +6 -1
- package/src/services/pages/pageEligibility.ts +9 -1
- package/src/services/settingsBridge.ts +1 -0
- package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
- package/src/stores/apps.ts +22 -11
- package/src/stores/settings.ts +70 -10
- package/src/types/capabilities/even-realities-g1.ts +4 -0
- package/src/types/capabilities/memomind-one.ts +43 -0
- package/src/types/capabilities/rayneo-io.ts +3 -2
- package/src/types/enums.ts +5 -0
- package/src/types/hardware.ts +14 -1
- package/src/utils/display/index.ts +4 -2
- package/src/utils/display/profiles/index.ts +9 -2
- package/src/utils/display/profiles/rayneo-io.ts +6 -1
- package/src/utils/display/scene/index.ts +1 -1
- package/src/utils/display/scene/process.ts +22 -7
- package/src/utils/miniappGlobals.ts +32 -64
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
- package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
package/src/services/JSRouter.ts
CHANGED
|
@@ -146,6 +146,9 @@ export class JSRouter {
|
|
|
146
146
|
*/
|
|
147
147
|
uiRouter: UIRouter | null = null
|
|
148
148
|
|
|
149
|
+
/** Teardowns in progress, so a stop and a racing start or stop see the same one. */
|
|
150
|
+
private readonly unregistering = new Map<string, Promise<void>>()
|
|
151
|
+
|
|
149
152
|
constructor(
|
|
150
153
|
private readonly runtime: LocalMiniappRuntime,
|
|
151
154
|
private readonly crust: JSCrustBinding,
|
|
@@ -351,12 +354,26 @@ export class JSRouter {
|
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
/**
|
|
354
|
-
* Tear down a JSContext and unregister from the runtime.
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
+
* Tear down a JSContext and unregister from the runtime. The session's
|
|
358
|
+
* `onBeforeDisconnect` handlers get a short grace window first, so a
|
|
359
|
+
* miniapp can clear what it drew and end its own work while its messages
|
|
360
|
+
* still reach the host. Concurrent calls share one teardown.
|
|
357
361
|
*/
|
|
358
|
-
|
|
359
|
-
if (!this.registered.has(packageName)) return
|
|
362
|
+
unregister(packageName: string): Promise<void> {
|
|
363
|
+
if (!this.registered.has(packageName)) return Promise.resolve()
|
|
364
|
+
const inFlight = this.unregistering.get(packageName)
|
|
365
|
+
if (inFlight) return inFlight
|
|
366
|
+
const teardown = this.teardown(packageName).finally(() => this.unregistering.delete(packageName))
|
|
367
|
+
this.unregistering.set(packageName, teardown)
|
|
368
|
+
return teardown
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Resolves once an in-flight {@link unregister} of this package has finished. */
|
|
372
|
+
async whenUnregistered(packageName: string): Promise<void> {
|
|
373
|
+
await this.unregistering.get(packageName)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async teardown(packageName: string): Promise<void> {
|
|
360
377
|
// Cancel any pending crash-respawn before tearing down — we don't
|
|
361
378
|
// want a stale timer to re-spawn a miniapp the user just disabled.
|
|
362
379
|
const pending = this.respawnTimers.get(packageName)
|
|
@@ -364,6 +381,7 @@ export class JSRouter {
|
|
|
364
381
|
BgTimer.clearTimeout(pending)
|
|
365
382
|
this.respawnTimers.delete(packageName)
|
|
366
383
|
}
|
|
384
|
+
await this.runtime.notifyWillDisconnect(packageName, "stopped")
|
|
367
385
|
this.spawnCache.delete(packageName)
|
|
368
386
|
this.crashController?.onKill(packageName)
|
|
369
387
|
// User-initiated close/disable: end any active nav trip so the glasses
|
|
@@ -382,6 +400,14 @@ export class JSRouter {
|
|
|
382
400
|
}
|
|
383
401
|
|
|
384
402
|
/** List packages the router has registered. */
|
|
403
|
+
/** The user opened a running miniapp again; the source rides the next respawn too. */
|
|
404
|
+
notifyLaunch(packageName: string, launchSource: MiniappLaunchSource): void {
|
|
405
|
+
if (!this.registered.has(packageName)) return
|
|
406
|
+
const cached = this.spawnCache.get(packageName)
|
|
407
|
+
if (cached) cached.launchSource = launchSource
|
|
408
|
+
this.runtime.sendLaunchChange(packageName, launchSource)
|
|
409
|
+
}
|
|
410
|
+
|
|
385
411
|
registeredPackages(): string[] {
|
|
386
412
|
return Array.from(this.registered)
|
|
387
413
|
}
|
|
@@ -483,13 +509,18 @@ export class JSRouter {
|
|
|
483
509
|
if (iface === "__error") {
|
|
484
510
|
const payload = this.tryParseArgs(msg.argsJson)
|
|
485
511
|
this.logger.error(`[${packageName}] ${method}`, payload)
|
|
512
|
+
// Crashes reach the `glassly dev` terminal too, not only console calls.
|
|
513
|
+
const summary = this.summarizeReason(payload, method)
|
|
514
|
+
const stack = (payload as {stack?: unknown} | null)?.stack
|
|
515
|
+
const errorArgs = typeof stack === "string" && stack ? [summary, stack] : [summary]
|
|
516
|
+
devServerBridge.forwardLog(packageName, "error", errorArgs, Date.now(), "background")
|
|
486
517
|
if (this.crashController) {
|
|
487
518
|
// Only treat "exception" + "unhandledRejection" + "uncaught" as
|
|
488
519
|
// crash signals. `console.error` calls also flow through the
|
|
489
520
|
// log path — those don't end the JSContext.
|
|
490
521
|
const isCrash = method === "exception" || method === "unhandledRejection" || method === "uncaught"
|
|
491
522
|
if (isCrash) {
|
|
492
|
-
this.handleCrash(packageName,
|
|
523
|
+
this.handleCrash(packageName, summary)
|
|
493
524
|
}
|
|
494
525
|
}
|
|
495
526
|
return
|
|
@@ -627,6 +627,9 @@ class LocalDisplayManager {
|
|
|
627
627
|
this.currentDisplay &&
|
|
628
628
|
this.currentDisplay.packageName === this.backgroundLock.packageName
|
|
629
629
|
if (bgHoldsAndDisplays) {
|
|
630
|
+
// The retained scene is now older than the saved frame; forget it so
|
|
631
|
+
// the restore paints this frame rather than replaying the stale one.
|
|
632
|
+
sceneRenderer.clearApp(packageName)
|
|
630
633
|
resolve?.({status: "blocked", reason: "a background app holds the display"})
|
|
631
634
|
return
|
|
632
635
|
}
|
|
@@ -974,8 +977,8 @@ class LocalDisplayManager {
|
|
|
974
977
|
}, delay)
|
|
975
978
|
}
|
|
976
979
|
} else {
|
|
977
|
-
// Retained state gone (app stopped
|
|
978
|
-
// elements as a fresh scene instead.
|
|
980
|
+
// Retained state gone (app stopped, or its newer frame was blocked) —
|
|
981
|
+
// re-render the saved elements as a fresh scene instead.
|
|
979
982
|
this.sendNow(saved.packageName, {view: "main", scene: saved.scene, durationMs: remaining})
|
|
980
983
|
}
|
|
981
984
|
return
|
|
@@ -57,6 +57,7 @@ import type {AiTier, AiTierProvider, SavedLlmKeys} from "./AiTierResolution"
|
|
|
57
57
|
import {decideLlmRoute, localLlmReason, type LocalLlmReason} from "./LlmRouting"
|
|
58
58
|
import localLlmModelManager from "./LocalLlmModelManager"
|
|
59
59
|
import localLlmProvider from "./LocalLlmProvider"
|
|
60
|
+
import offlineActivity from "./OfflineActivity"
|
|
60
61
|
import {HttpError} from "@glassly/cloud-client"
|
|
61
62
|
import type {
|
|
62
63
|
QuotaStatusPayload,
|
|
@@ -304,6 +305,20 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | nul
|
|
|
304
305
|
})
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
/** Bounds the cold-GPS wait for a one-shot poll; on a stall, any older fix beats an error. */
|
|
309
|
+
const FRESH_LOCATION_TIMEOUT_MS = 8_000
|
|
310
|
+
|
|
311
|
+
async function freshLocationOrStale(): Promise<Location.LocationObject> {
|
|
312
|
+
const fresh = await withTimeout(
|
|
313
|
+
Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Low}),
|
|
314
|
+
FRESH_LOCATION_TIMEOUT_MS,
|
|
315
|
+
)
|
|
316
|
+
if (fresh) return fresh
|
|
317
|
+
const stale = await Location.getLastKnownPositionAsync()
|
|
318
|
+
if (stale) return stale
|
|
319
|
+
throw new Error("No location fix available")
|
|
320
|
+
}
|
|
321
|
+
|
|
307
322
|
const LOG_TAG = "LOCAL_MINIAPP"
|
|
308
323
|
const DIAGNOSTIC_MAX_LIST_ITEMS = 100
|
|
309
324
|
const DIAGNOSTIC_MAX_STRING_LENGTH = 512
|
|
@@ -326,6 +341,8 @@ const SYSTEM_MINIAPP_PACKAGES = new Set([
|
|
|
326
341
|
"com.glassly.miniappdev",
|
|
327
342
|
])
|
|
328
343
|
const PING_INTERVAL_MS = 5_000
|
|
344
|
+
/** How long a stopping miniapp's `beforeDisconnect` handlers get before teardown. */
|
|
345
|
+
const WILL_DISCONNECT_GRACE_MS = 100
|
|
329
346
|
// Min gap between "transcription limit reached" notices on the glasses. A
|
|
330
347
|
// debounce, not a cooldown: the cloud re-announces the block per refused
|
|
331
348
|
// subscribe, and miniapps retry in bursts — this only coalesces those bursts
|
|
@@ -1235,21 +1252,26 @@ class LocalMiniappRuntime {
|
|
|
1235
1252
|
* undeliverable anyway.
|
|
1236
1253
|
*/
|
|
1237
1254
|
public async gracefullyUnregisterApp(packageName: string, reason = "unregistering"): Promise<void> {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
console.log(`${LOG_TAG}: gracefullyUnregisterApp(${packageName}) — sending WILL_DISCONNECT`)
|
|
1242
|
-
this.sendToMiniapp(packageName, {
|
|
1243
|
-
type: MiniappResponseType.WILL_DISCONNECT,
|
|
1244
|
-
reason,
|
|
1245
|
-
})
|
|
1246
|
-
// 50 ms: imperceptible to the user, plenty of time for the SDK's
|
|
1247
|
-
// synchronous `beforeDisconnect` handlers to flush a final
|
|
1248
|
-
// sendOneShot through the still-open transport.
|
|
1249
|
-
await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, 50))
|
|
1255
|
+
if (!this.connectedApps.has(packageName)) return
|
|
1256
|
+
await this.notifyWillDisconnect(packageName, reason)
|
|
1250
1257
|
this.unregisterApp(packageName)
|
|
1251
1258
|
}
|
|
1252
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* Send `WILL_DISCONNECT` and wait out the grace window, so the session's
|
|
1262
|
+
* `beforeDisconnect` handlers run (Navigation clears its HUD and ends its
|
|
1263
|
+
* trip) and their last messages land before teardown. No-op unless the
|
|
1264
|
+
* app finished its handshake.
|
|
1265
|
+
*/
|
|
1266
|
+
public async notifyWillDisconnect(packageName: string, reason: string): Promise<void> {
|
|
1267
|
+
if (!this.connectedApps.has(packageName) || !this.handshookApps.has(packageName)) return
|
|
1268
|
+
console.log(`${LOG_TAG}: ${packageName} — sending WILL_DISCONNECT (${reason})`)
|
|
1269
|
+
this.sendToMiniapp(packageName, {type: MiniappResponseType.WILL_DISCONNECT, reason})
|
|
1270
|
+
// The message and the replies it triggers each cross a JSContext hop;
|
|
1271
|
+
// short enough not to slow an app switch noticeably.
|
|
1272
|
+
await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, WILL_DISCONNECT_GRACE_MS))
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1253
1275
|
public unregisterApp(packageName: string, opts?: {endNavigation?: boolean}): void {
|
|
1254
1276
|
console.log(`${LOG_TAG}: unregisterApp(${packageName})`)
|
|
1255
1277
|
const releasedMicGateOverride = micStateCoordinator.clearMiniappGateOverrides(packageName)
|
|
@@ -1659,11 +1681,11 @@ class LocalMiniappRuntime {
|
|
|
1659
1681
|
this.handleOpenUrl(packageName, payload)
|
|
1660
1682
|
break
|
|
1661
1683
|
case MiniappRequestType.DISPLAY_DIAGNOSTICS: {
|
|
1662
|
-
// The records are parsed out of the G2PERF lines, which the SDK only logs while
|
|
1684
|
+
// The records are parsed out of the G2PERF lines, which the SDK only logs while perf_logs is on.
|
|
1663
1685
|
const settings = useSettingsStore.getState()
|
|
1664
1686
|
const enabled =
|
|
1665
1687
|
settings.getSetting(SETTINGS.debug_console.key) === true &&
|
|
1666
|
-
settings.getSetting(SETTINGS.
|
|
1688
|
+
settings.getSetting(SETTINGS.perf_logs.key) === true
|
|
1667
1689
|
this.sendResult(packageName, requestId, true, {
|
|
1668
1690
|
enabled,
|
|
1669
1691
|
generatedAt: Date.now(),
|
|
@@ -3450,7 +3472,7 @@ class LocalMiniappRuntime {
|
|
|
3450
3472
|
const locationStartedAt = Date.now()
|
|
3451
3473
|
const cached = await Location.getLastKnownPositionAsync({maxAge: 60_000})
|
|
3452
3474
|
console.log(`${LOG_TAG}: location poll cache hit=${cached !== null} elapsed=${Date.now() - locationStartedAt}ms`)
|
|
3453
|
-
const location = cached ?? (await
|
|
3475
|
+
const location = cached ?? (await freshLocationOrStale())
|
|
3454
3476
|
console.log(`${LOG_TAG}: location poll resolved elapsed=${Date.now() - locationStartedAt}ms`)
|
|
3455
3477
|
|
|
3456
3478
|
this.sendResult(packageName, requestId, true, {
|
|
@@ -4322,6 +4344,7 @@ class LocalMiniappRuntime {
|
|
|
4322
4344
|
return
|
|
4323
4345
|
}
|
|
4324
4346
|
|
|
4347
|
+
const endActivity = offlineActivity.beginLlm(reason, localLlmModelManager.model(modelId).label)
|
|
4325
4348
|
try {
|
|
4326
4349
|
// webSearch is a vendor server-side grant; the phone has none.
|
|
4327
4350
|
const {webSearch: _webSearch, ...localRequest} = request
|
|
@@ -4346,6 +4369,8 @@ class LocalMiniappRuntime {
|
|
|
4346
4369
|
undefined,
|
|
4347
4370
|
primaryError ?? {code: MiniappErrorCode.AI_LOCAL_UNAVAILABLE, message},
|
|
4348
4371
|
)
|
|
4372
|
+
} finally {
|
|
4373
|
+
endActivity()
|
|
4349
4374
|
}
|
|
4350
4375
|
}
|
|
4351
4376
|
|
|
@@ -6071,13 +6096,16 @@ class LocalMiniappRuntime {
|
|
|
6071
6096
|
private async startInteropApp(packageName: string): Promise<boolean> {
|
|
6072
6097
|
const app = this.interopApps().find((a) => a.packageName === packageName)
|
|
6073
6098
|
if (!app) return false
|
|
6074
|
-
//
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
}
|
|
6080
|
-
|
|
6099
|
+
// session.miniapps.start comes from the glasses (the dashboard's Apps menu,
|
|
6100
|
+
// an Assist voice command), unlike a headless action wake, which carries no source.
|
|
6101
|
+
// The full start, as a phone open runs it: the standard peer stops so this
|
|
6102
|
+
// app owns the display, and an app already running hears the new launch.
|
|
6103
|
+
const launchSource = "glasses" as const
|
|
6104
|
+
const started = await useAppStatusStore.getState().start(app, {skipNavigation: true, launchSource})
|
|
6105
|
+
if (!started) return false
|
|
6106
|
+
// The caller may message the app next; start() resolves at spawn.
|
|
6107
|
+
if (app.local) await miniappLauncher.ensureConnected(packageName, undefined, {launchSource})
|
|
6108
|
+
return true
|
|
6081
6109
|
}
|
|
6082
6110
|
|
|
6083
6111
|
private stopInteropApp(packageName: string): Promise<void> {
|
|
@@ -6851,6 +6879,17 @@ class LocalMiniappRuntime {
|
|
|
6851
6879
|
this.sendToMiniapp(packageName, {type: MiniappResponseType.LINK, params})
|
|
6852
6880
|
}
|
|
6853
6881
|
|
|
6882
|
+
/**
|
|
6883
|
+
* Tell a running miniapp the user opened it again and from where. The
|
|
6884
|
+
* CONNECT_ACK only carries the source of the spawn, which is stale for a
|
|
6885
|
+
* background app that keeps running between opens.
|
|
6886
|
+
*/
|
|
6887
|
+
public sendLaunchChange(packageName: string, launchSource: MiniappLaunchSource): void {
|
|
6888
|
+
const existing = this.connectedApps.get(packageName)
|
|
6889
|
+
if (existing) existing.launchSource = launchSource
|
|
6890
|
+
this.sendToMiniapp(packageName, {type: MiniappResponseType.LAUNCH_CHANGE, launchSource})
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6854
6893
|
/**
|
|
6855
6894
|
* Send a VISIBILITY_CHANGE push to a miniapp.
|
|
6856
6895
|
*/
|
|
@@ -2,6 +2,7 @@ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
|
2
2
|
import {ISLAND_SETTINGS_KEYS} from "../runtime/config"
|
|
3
3
|
import {useSettingsStore} from "../stores/settings"
|
|
4
4
|
import {cloudClientService} from "./CloudClientService"
|
|
5
|
+
import offlineActivity from "./OfflineActivity"
|
|
5
6
|
import sttModelManager from "./STTModelManager"
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -223,6 +224,9 @@ class LocalSttFallbackCoordinator {
|
|
|
223
224
|
|
|
224
225
|
private publishReason(): void {
|
|
225
226
|
const reason = this.localActive ? this.currentReason() : ""
|
|
227
|
+
offlineActivity.setTranscription(
|
|
228
|
+
this.localActive ? {reason: this.currentReason(), model: sttModelManager.currentModelLabel()} : null,
|
|
229
|
+
)
|
|
226
230
|
const store = useSettingsStore.getState()
|
|
227
231
|
if (store.getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason) !== reason) {
|
|
228
232
|
store.setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, reason)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import offlineActivity, {type ServedTranslationPair} from "./OfflineActivity"
|
|
1
2
|
import {offlineTranslationManager} from "./OfflineTranslationManager"
|
|
2
3
|
|
|
3
4
|
/** One concrete-target `translation:<source>:<target>` stream and who listens to it. */
|
|
@@ -67,6 +68,7 @@ export class LocalTranslationService {
|
|
|
67
68
|
|
|
68
69
|
attach(host: Host): void {
|
|
69
70
|
this.host = host
|
|
71
|
+
offlineActivity.setTranslationSource(() => this.servedPairs())
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
setSubscriptions(subscriptions: LocalTranslationSubscription[]): void {
|
|
@@ -75,6 +77,24 @@ export class LocalTranslationService {
|
|
|
75
77
|
for (const key of this.lanes.keys()) {
|
|
76
78
|
if (!live.has(key.substring(key.indexOf(">") + 1))) this.lanes.delete(key)
|
|
77
79
|
}
|
|
80
|
+
offlineActivity.refresh()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
servedPairs(): ServedTranslationPair[] {
|
|
84
|
+
if (!this.host || !offlineTranslationManager.isSupported()) return []
|
|
85
|
+
const pairs = new Map<string, ServedTranslationPair>()
|
|
86
|
+
let primaryUp: boolean | null = null
|
|
87
|
+
for (const sub of this.subscriptions) {
|
|
88
|
+
if (baseLanguage(sub.source) === baseLanguage(sub.target)) continue
|
|
89
|
+
if (!sub.forceLocal) {
|
|
90
|
+
primaryUp ??= this.host.primaryUp()
|
|
91
|
+
if (primaryUp || !sub.cloud) continue
|
|
92
|
+
}
|
|
93
|
+
const key = `${sub.source}>${sub.target}`
|
|
94
|
+
const existing = pairs.get(key)
|
|
95
|
+
pairs.set(key, {source: sub.source, target: sub.target, forceLocal: sub.forceLocal || !!existing?.forceLocal})
|
|
96
|
+
}
|
|
97
|
+
return [...pairs.values()]
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
onLocalTranscript(transcript: LocalTranscript, sourceLanguage: string): void {
|
|
@@ -29,42 +29,77 @@ export interface MiniappEngine {
|
|
|
29
29
|
|
|
30
30
|
let engine: MiniappEngine | null = null
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Kill and re-spawn a miniapp's background JSContext from its installed bundle
|
|
34
|
+
* (dev: the dev server's fresh build). Resolves false when it isn't installed
|
|
35
|
+
* or the spawn failed.
|
|
36
|
+
*/
|
|
37
|
+
async function respawnBackground(router: JSRouter, uiRouter: UIRouter, packageName: string): Promise<boolean> {
|
|
38
|
+
// Re-resolve the freshly built bundle (dev: HTTP off the dev server;
|
|
39
|
+
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
40
|
+
// the declared permissions + manifest across the respawn — omitting them
|
|
41
|
+
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
42
|
+
// per-call dispatch checks would start rejecting after a background
|
|
43
|
+
// hot-reload.
|
|
44
|
+
const resolved = await miniappLauncher.resolveBundle(packageName)
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
50
|
+
// which is idempotent and would no-op an already-registered context.
|
|
51
|
+
await router.unregister(packageName)
|
|
52
|
+
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
53
|
+
permissions: resolved.declaredPermissions,
|
|
54
|
+
installedManifest: resolved.installedManifest,
|
|
55
|
+
})
|
|
56
|
+
if (!ok) {
|
|
57
|
+
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
61
|
+
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
62
|
+
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
63
|
+
// instead of being dropped while unbound.
|
|
64
|
+
uiRouter.notifyReopen(packageName)
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
function wireDevServerRespawnBackground(router: JSRouter, uiRouter: UIRouter): void {
|
|
33
69
|
devServerBridge.onRespawnBackground(async (packageName) => {
|
|
34
70
|
try {
|
|
35
|
-
|
|
36
|
-
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
37
|
-
// the declared permissions + manifest across the respawn — omitting them
|
|
38
|
-
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
39
|
-
// per-call dispatch checks would start rejecting after a background
|
|
40
|
-
// hot-reload.
|
|
41
|
-
const resolved = await miniappLauncher.resolveBundle(packageName)
|
|
42
|
-
if (!resolved) {
|
|
43
|
-
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
47
|
-
// which is idempotent and would no-op an already-registered context.
|
|
48
|
-
await router.unregister(packageName)
|
|
49
|
-
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
50
|
-
permissions: resolved.declaredPermissions,
|
|
51
|
-
installedManifest: resolved.installedManifest,
|
|
52
|
-
})
|
|
53
|
-
if (!ok) {
|
|
54
|
-
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
58
|
-
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
59
|
-
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
60
|
-
// instead of being dropped while unbound.
|
|
61
|
-
uiRouter.notifyReopen(packageName)
|
|
71
|
+
await respawnBackground(router, uiRouter, packageName)
|
|
62
72
|
} catch (e) {
|
|
63
73
|
console.warn(`JSRuntime: respawn-bg threw for ${packageName}:`, e)
|
|
64
74
|
}
|
|
65
75
|
})
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
/** Packages with a live background JSContext. */
|
|
79
|
+
export function runningMiniappPackages(): string[] {
|
|
80
|
+
return engine?.router.registeredPackages() ?? []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Tear down a miniapp's background JSContext; a no-op when it isn't running. */
|
|
84
|
+
export async function stopMiniapp(packageName: string): Promise<void> {
|
|
85
|
+
await engine?.router.unregister(packageName)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Start (or restart) a miniapp's background so it reads its storage afresh (an
|
|
90
|
+
* app state restore). Resolves false when the engine isn't up or the spawn failed.
|
|
91
|
+
*/
|
|
92
|
+
export async function respawnMiniapp(packageName: string): Promise<boolean> {
|
|
93
|
+
const current = engine
|
|
94
|
+
if (!current) return false
|
|
95
|
+
try {
|
|
96
|
+
return await respawnBackground(current.router, current.uiRouter, packageName)
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.warn(`JSRuntime: respawn threw for ${packageName}:`, e)
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
68
103
|
/**
|
|
69
104
|
* Construct (once) and return the JSRuntime engine singletons. Starts the router's
|
|
70
105
|
* message pump on first call. Subsequent calls return the same instances.
|
|
@@ -91,6 +91,11 @@ class MiniappLauncher {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** True iff the package's background JS context is currently spawned. */
|
|
94
|
+
/** Tell an already-running miniapp the user opened it again and from where. */
|
|
95
|
+
notifyLaunch(packageName: string, launchSource: MiniappLaunchSource): void {
|
|
96
|
+
this.deps?.router.notifyLaunch(packageName, launchSource)
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
isRunning(packageName: string): boolean {
|
|
95
100
|
return this.deps?.router.registeredPackages().includes(packageName) ?? false
|
|
96
101
|
}
|
|
@@ -228,6 +233,8 @@ class MiniappLauncher {
|
|
|
228
233
|
*/
|
|
229
234
|
async ensureRunning(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {
|
|
230
235
|
const router = this.requireRouter()
|
|
236
|
+
// A context still tearing down from a stop is not one to reuse.
|
|
237
|
+
await router.whenUnregistered(packageName)
|
|
231
238
|
|
|
232
239
|
// Already spawned: best-effort resolve for the UI entry, never throw —
|
|
233
240
|
// a headless caller that doesn't need UI gets {null, null} fast even if
|
|
@@ -16,6 +16,7 @@ import CrustModule from "@glassly/crust"
|
|
|
16
16
|
import {resolveMapboxAccessToken, resolveMapboxRenderToken, userMapboxApiKey} from "./MapboxTokenService"
|
|
17
17
|
import {decodePolyline, parseDurationSeconds} from "./navigation/routesApiCodec"
|
|
18
18
|
import {resolveStepRoads} from "./navigation/roadNameResolver"
|
|
19
|
+
import {BgTimer} from "../utils/timers"
|
|
19
20
|
|
|
20
21
|
const LOG_TAG = "NAV_SERVICE"
|
|
21
22
|
|
|
@@ -394,7 +395,7 @@ class NavigationService {
|
|
|
394
395
|
sessionToken: string,
|
|
395
396
|
): Promise<{
|
|
396
397
|
ok: boolean
|
|
397
|
-
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string}>
|
|
398
|
+
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string; distanceMeters?: number}>
|
|
398
399
|
error?: string
|
|
399
400
|
}> {
|
|
400
401
|
return placeAutocompleteViaSearchBox(query, near, sessionToken)
|
|
@@ -506,6 +507,19 @@ export default navigationService
|
|
|
506
507
|
const MAPBOX_DIRECTIONS_BASE = "https://api.mapbox.com/directions/v5/mapbox"
|
|
507
508
|
const MAPBOX_GEOCODE_REVERSE = "https://api.mapbox.com/search/geocode/v6/reverse"
|
|
508
509
|
const MAPBOX_SEARCHBOX_SUGGEST = "https://api.mapbox.com/search/searchbox/v1/suggest"
|
|
510
|
+
|
|
511
|
+
/** Place search sits in front of a voice command; a stalled call must fail over, not hang. */
|
|
512
|
+
const PLACE_SEARCH_TIMEOUT_MS = 6_000
|
|
513
|
+
|
|
514
|
+
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
515
|
+
const controller = new AbortController()
|
|
516
|
+
const timer = BgTimer.setTimeout(() => controller.abort(), timeoutMs)
|
|
517
|
+
try {
|
|
518
|
+
return await fetch(url, {method: "GET", signal: controller.signal})
|
|
519
|
+
} finally {
|
|
520
|
+
BgTimer.clearTimeout(timer)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
509
523
|
const MAPBOX_SEARCHBOX_RETRIEVE = "https://api.mapbox.com/search/searchbox/v1/retrieve"
|
|
510
524
|
|
|
511
525
|
/**
|
|
@@ -818,7 +832,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
818
832
|
sessionToken: string,
|
|
819
833
|
): Promise<{
|
|
820
834
|
ok: boolean
|
|
821
|
-
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string}>
|
|
835
|
+
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string; distanceMeters?: number}>
|
|
822
836
|
error?: string
|
|
823
837
|
}> {
|
|
824
838
|
if (!query.trim()) return {ok: true, suggestions: []}
|
|
@@ -837,11 +851,14 @@ async function placeAutocompleteViaSearchBox(
|
|
|
837
851
|
types: "poi,address,place,street",
|
|
838
852
|
limit: "10",
|
|
839
853
|
})
|
|
840
|
-
// Mapbox wants lng,lat for proximity bias.
|
|
841
|
-
if (near)
|
|
854
|
+
// Mapbox wants lng,lat for proximity bias; `origin` adds per-suggestion distance.
|
|
855
|
+
if (near) {
|
|
856
|
+
params.set("proximity", `${near.lng},${near.lat}`)
|
|
857
|
+
params.set("origin", `${near.lng},${near.lat}`)
|
|
858
|
+
}
|
|
842
859
|
|
|
843
860
|
try {
|
|
844
|
-
const res = await
|
|
861
|
+
const res = await fetchWithTimeout(`${MAPBOX_SEARCHBOX_SUGGEST}?${params.toString()}`, PLACE_SEARCH_TIMEOUT_MS)
|
|
845
862
|
if (!res.ok) return {ok: false, error: `Search Box suggest ${res.status}`}
|
|
846
863
|
const json = (await res.json()) as {
|
|
847
864
|
suggestions?: Array<{
|
|
@@ -849,6 +866,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
849
866
|
name?: string
|
|
850
867
|
place_formatted?: string
|
|
851
868
|
full_address?: string
|
|
869
|
+
distance?: number
|
|
852
870
|
}>
|
|
853
871
|
}
|
|
854
872
|
const suggestions = (json.suggestions ?? [])
|
|
@@ -857,6 +875,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
857
875
|
placeId: s.mapbox_id,
|
|
858
876
|
mainText: s.name ?? "",
|
|
859
877
|
secondaryText: s.place_formatted ?? s.full_address ?? "",
|
|
878
|
+
...(typeof s.distance === "number" ? {distanceMeters: s.distance} : {}),
|
|
860
879
|
}))
|
|
861
880
|
return {ok: true, suggestions}
|
|
862
881
|
} catch (err) {
|
|
@@ -890,9 +909,10 @@ async function placeDetailsViaSearchBox(
|
|
|
890
909
|
}
|
|
891
910
|
const params = new URLSearchParams({access_token: token, session_token: sessionToken})
|
|
892
911
|
try {
|
|
893
|
-
const res = await
|
|
894
|
-
|
|
895
|
-
|
|
912
|
+
const res = await fetchWithTimeout(
|
|
913
|
+
`${MAPBOX_SEARCHBOX_RETRIEVE}/${encodeURIComponent(placeId)}?${params.toString()}`,
|
|
914
|
+
PLACE_SEARCH_TIMEOUT_MS,
|
|
915
|
+
)
|
|
896
916
|
if (!res.ok) return {ok: false, error: `Search Box retrieve ${res.status}`}
|
|
897
917
|
const json = (await res.json()) as {
|
|
898
918
|
features?: Array<{
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the phone is running on-device right now (transcription, translation,
|
|
3
|
+
* LLM) and why, for the home offline card. Producers push in; it imports
|
|
4
|
+
* nothing at runtime so their test stubs stay unaffected.
|
|
5
|
+
*/
|
|
6
|
+
import type {LocalLlmReason} from "./LlmRouting"
|
|
7
|
+
import type {LocalSttFallbackReason} from "./LocalSttFallbackCoordinator"
|
|
8
|
+
|
|
9
|
+
export type OfflineActivityReason = LocalSttFallbackReason | LocalLlmReason
|
|
10
|
+
|
|
11
|
+
export interface OfflineTranscriptionActivity {
|
|
12
|
+
reason: LocalSttFallbackReason
|
|
13
|
+
model: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OfflineTranslationPair {
|
|
17
|
+
source: string
|
|
18
|
+
target: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OfflineTranslationActivity {
|
|
22
|
+
reason: OfflineActivityReason
|
|
23
|
+
pairs: OfflineTranslationPair[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OfflineLlmActivity {
|
|
27
|
+
reason: LocalLlmReason
|
|
28
|
+
model: string
|
|
29
|
+
requests: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OfflineActivity {
|
|
33
|
+
transcription: OfflineTranscriptionActivity | null
|
|
34
|
+
translation: OfflineTranslationActivity | null
|
|
35
|
+
llm: OfflineLlmActivity | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ServedTranslationPair extends OfflineTranslationPair {
|
|
39
|
+
forceLocal: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EMPTY: OfflineActivity = {transcription: null, translation: null, llm: null}
|
|
43
|
+
|
|
44
|
+
class OfflineActivityTracker {
|
|
45
|
+
private transcription: OfflineTranscriptionActivity | null = null
|
|
46
|
+
private llmRuns: Array<{reason: LocalLlmReason; model: string}> = []
|
|
47
|
+
private translationSource: (() => ServedTranslationPair[]) | null = null
|
|
48
|
+
private snapshot: OfflineActivity = EMPTY
|
|
49
|
+
private snapshotKey = JSON.stringify(EMPTY)
|
|
50
|
+
private listeners = new Set<(activity: OfflineActivity) => void>()
|
|
51
|
+
|
|
52
|
+
current(): OfflineActivity {
|
|
53
|
+
return this.snapshot
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
subscribe(listener: (activity: OfflineActivity) => void): () => void {
|
|
57
|
+
this.listeners.add(listener)
|
|
58
|
+
return () => {
|
|
59
|
+
this.listeners.delete(listener)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setTranscription(transcription: OfflineTranscriptionActivity | null): void {
|
|
64
|
+
this.transcription = transcription
|
|
65
|
+
this.refresh()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beginLlm(reason: LocalLlmReason, model: string): () => void {
|
|
69
|
+
const run = {reason, model}
|
|
70
|
+
this.llmRuns.push(run)
|
|
71
|
+
this.refresh()
|
|
72
|
+
let ended = false
|
|
73
|
+
return () => {
|
|
74
|
+
if (ended) return
|
|
75
|
+
ended = true
|
|
76
|
+
this.llmRuns = this.llmRuns.filter((r) => r !== run)
|
|
77
|
+
this.refresh()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setTranslationSource(source: (() => ServedTranslationPair[]) | null): void {
|
|
82
|
+
this.translationSource = source
|
|
83
|
+
this.refresh()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
refresh(): void {
|
|
87
|
+
const next: OfflineActivity = {
|
|
88
|
+
transcription: this.transcription,
|
|
89
|
+
translation: this.translation(),
|
|
90
|
+
llm: this.llm(),
|
|
91
|
+
}
|
|
92
|
+
const key = JSON.stringify(next)
|
|
93
|
+
if (key === this.snapshotKey) return
|
|
94
|
+
this.snapshotKey = key
|
|
95
|
+
this.snapshot = next
|
|
96
|
+
for (const listener of this.listeners) {
|
|
97
|
+
try {
|
|
98
|
+
listener(next)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.warn("OfflineActivity: listener failed:", error)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
reset(): void {
|
|
106
|
+
this.transcription = null
|
|
107
|
+
this.llmRuns = []
|
|
108
|
+
this.translationSource = null
|
|
109
|
+
this.refresh()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private translation(): OfflineTranslationActivity | null {
|
|
113
|
+
if (!this.transcription || !this.translationSource) return null
|
|
114
|
+
const pairs = this.translationSource()
|
|
115
|
+
if (pairs.length === 0) return null
|
|
116
|
+
return {
|
|
117
|
+
reason: pairs.some((p) => p.forceLocal) ? "force_local" : this.transcription.reason,
|
|
118
|
+
pairs: pairs.map(({source, target}) => ({source, target})),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private llm(): OfflineLlmActivity | null {
|
|
123
|
+
const latest = this.llmRuns[this.llmRuns.length - 1]
|
|
124
|
+
if (!latest) return null
|
|
125
|
+
return {reason: latest.reason, model: latest.model, requests: this.llmRuns.length}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export {OfflineActivityTracker}
|
|
130
|
+
const offlineActivity = new OfflineActivityTracker()
|
|
131
|
+
export default offlineActivity
|