| author | |
| committer | |
| log | cfddb4201df8941bbfde008919da70fe5ec7552b |
| tree | d75d4518e6deb7303cdd7220b787a2f1ab3d715c |
| parent | 57626eb6eaa436b22260068d23f4e3df5f389cd1 |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 119 insertions(+), 69 deletions(-)
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+9-12| ... | ... | @@ -46,6 +46,7 @@ import { |
| 46 | 46 | import { |
| 47 | 47 | findSharedSettingsMismatches, |
| 48 | 48 | pickSharedServerSettings, |
| 49 | supportsSharedSettingsSync, | |
| 49 | 50 | } from "@t3tools/client-runtime/state/shared-settings"; |
| 50 | 51 | import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; |
| 51 | 52 | import { |
| ... | ... | @@ -553,9 +554,9 @@ const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterD |
| 553 | 554 | |
| 554 | 555 | /** |
| 555 | 556 | * Auto-settlement is a user preference that every server has to hold. Mobile |
| 556 | * has no primary environment, so the first connected environment that | |
| 557 | * supports it is the reference value. Edits fan out to every connected | |
| 558 | * environment, and a mismatch row lets the user push the reference out. | |
| 557 | * has no primary environment, so the first eligible sync target provides the | |
| 558 | * reference value. Edits fan out to every eligible target, and a mismatch row | |
| 559 | * lets the user push the reference out. | |
| 559 | 560 | */ |
| 560 | 561 | function AutoSettleSettingsRows() { |
| 561 | 562 | const { environments } = useEnvironments(); |
| ... | ... | @@ -564,12 +565,8 @@ function AutoSettleSettingsRows() { |
| 564 | 565 | reportFailure: true, |
| 565 | 566 | }); |
| 566 | 567 | |
| 567 | const connected = environments.filter( | |
| 568 | (environment) => | |
| 569 | environment.connection.phase === "connected" && | |
| 570 | environment.serverConfig?.environment.capabilities.threadAutoSettlement === true, | |
| 571 | ); | |
| 572 | const reference = connected[0] ?? null; | |
| 568 | const syncTargets = environments.filter(supportsSharedSettingsSync); | |
| 569 | const reference = syncTargets[0] ?? null; | |
| 573 | 570 | const referenceSettings = reference?.serverConfig?.settings ?? null; |
| 574 | 571 | |
| 575 | 572 | const [daysDraft, setDaysDraft] = useState<string | null>(null); |
| ... | ... | @@ -579,7 +576,7 @@ function AutoSettleSettingsRows() { |
| 579 | 576 | } |
| 580 | 577 | |
| 581 | 578 | const writeToAll = (patch: ServerSettingsPatch) => { |
| 582 | for (const environment of connected) { | |
| 579 | for (const environment of syncTargets) { | |
| 583 | 580 | void updateSettings({ environmentId: environment.environmentId, input: { patch } }); |
| 584 | 581 | } |
| 585 | 582 | }; |
| ... | ... | @@ -590,7 +587,7 @@ function AutoSettleSettingsRows() { |
| 590 | 587 | environments: environments.map((environment) => ({ |
| 591 | 588 | environmentId: environment.environmentId, |
| 592 | 589 | label: environment.label, |
| 593 | connected: environment.connection.phase === "connected", | |
| 590 | syncEligible: supportsSharedSettingsSync(environment), | |
| 594 | 591 | settings: environment.serverConfig?.settings ?? null, |
| 595 | 592 | })), |
| 596 | 593 | }); |
| ... | ... | @@ -600,7 +597,7 @@ function AutoSettleSettingsRows() { |
| 600 | 597 | const draft = (daysDraft ?? "").trim(); |
| 601 | 598 | setDaysDraft(null); |
| 602 | 599 | // Whole-string check so "3.5" and "3days" are rejected instead of |
| 603 | // silently becoming 3 on every connected environment. | |
| 600 | // silently becoming 3 on every eligible sync target. | |
| 604 | 601 | const parsed = /^\d+$/.test(draft) ? Number(draft) : Number.NaN; |
| 605 | 602 | if ( |
| 606 | 603 | Number.isInteger(parsed) && |
apps/web/src/hooks/useSettings.ts+15-30| ... | ... | @@ -29,6 +29,7 @@ import { |
| 29 | 29 | findSharedSettingsMismatches, |
| 30 | 30 | pickSharedServerSettings, |
| 31 | 31 | splitSharedServerPatch, |
| 32 | supportsSharedSettingsSync, | |
| 32 | 33 | } from "@t3tools/client-runtime/state/shared-settings"; |
| 33 | 34 | import { ensureLocalApi } from "~/localApi"; |
| 34 | 35 | import { |
| ... | ... | @@ -42,11 +43,7 @@ import * as Struct from "effect/Struct"; |
| 42 | 43 | import { toastManager } from "~/components/ui/toast"; |
| 43 | 44 | import { isHostedStaticApp } from "~/hostedPairing"; |
| 44 | 45 | import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server"; |
| 45 | import { | |
| 46 | type EnvironmentPresentation, | |
| 47 | useEnvironments, | |
| 48 | usePrimaryEnvironment, | |
| 49 | } from "~/state/environments"; | |
| 46 | import { useEnvironments, usePrimaryEnvironment } from "~/state/environments"; | |
| 50 | 47 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 51 | 48 | import { useTheme } from "./useTheme"; |
| 52 | 49 | |
| ... | ... | @@ -332,26 +329,14 @@ export function usePrimarySettingsAvailable(): boolean { |
| 332 | 329 | return primaryEnvironment !== null || !isHostedStaticApp(); |
| 333 | 330 | } |
| 334 | 331 | |
| 335 | /** | |
| 336 | * Whether an environment can hold every shared key right now. Gated on the | |
| 337 | * auto-settlement capability because it is the newest of the shared keys: a | |
| 338 | * server that has it has all of them. Older servers drop unknown keys on | |
| 339 | * write, so a mismatch against them could never clear, and their decoded | |
| 340 | * defaults must not be treated as real values. | |
| 341 | */ | |
| 342 | function supportsSharedSettings(environment: EnvironmentPresentation): boolean { | |
| 343 | return ( | |
| 344 | environment.connection.phase === "connected" && | |
| 345 | environment.serverConfig?.environment.capabilities.threadAutoSettlement === true | |
| 346 | ); | |
| 347 | } | |
| 348 | ||
| 349 | 332 | /** Environments that can receive a shared settings write right now. */ |
| 350 | function useConnectedEnvironmentIds(): ReadonlyArray<EnvironmentId> { | |
| 333 | function useSharedSettingsSyncTargetIds(): ReadonlyArray<EnvironmentId> { | |
| 351 | 334 | const { environments } = useEnvironments(); |
| 352 | 335 | return useMemo( |
| 353 | 336 | () => |
| 354 | environments.filter(supportsSharedSettings).map((environment) => environment.environmentId), | |
| 337 | environments | |
| 338 | .filter(supportsSharedSettingsSync) | |
| 339 | .map((environment) => environment.environmentId), | |
| 355 | 340 | [environments], |
| 356 | 341 | ); |
| 357 | 342 | } |
| ... | ... | @@ -361,16 +346,16 @@ function useConnectedEnvironmentIds(): ReadonlyArray<EnvironmentId> { |
| 361 | 346 | * |
| 362 | 347 | * Server keys are optimistically patched in atom-backed server state, then |
| 363 | 348 | * persisted via RPC. Shared server keys (see `SHARED_SERVER_SETTING_KEYS`) |
| 364 | * are written to every connected environment, not only the target, so a user | |
| 365 | * preference does not silently drift between machines. Client keys go through | |
| 366 | * client persistence. | |
| 349 | * are written to every eligible sync target, not only the selected target, so | |
| 350 | * a user preference does not silently drift between machines. Client keys go | |
| 351 | * through client persistence. | |
| 367 | 352 | */ |
| 368 | 353 | function useUpdateSettingsTarget(environmentId: EnvironmentId | null) { |
| 369 | 354 | const persistServerSettings = useAtomCommand( |
| 370 | 355 | serverEnvironment.updateSettings, |
| 371 | 356 | "server settings update", |
| 372 | 357 | ); |
| 373 | const connectedEnvironmentIds = useConnectedEnvironmentIds(); | |
| 358 | const sharedSettingsSyncTargetIds = useSharedSettingsSyncTargetIds(); | |
| 374 | 359 | const updateSettings = useCallback( |
| 375 | 360 | (patch: UnifiedSettingsPatch) => { |
| 376 | 361 | const { serverPatch, clientPatch } = splitPatch(patch); |
| ... | ... | @@ -395,7 +380,7 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) { |
| 395 | 380 | } |
| 396 | 381 | } |
| 397 | 382 | if (Object.keys(sharedPatch).length > 0) { |
| 398 | const targets = new Set(connectedEnvironmentIds); | |
| 383 | const targets = new Set(sharedSettingsSyncTargetIds); | |
| 399 | 384 | if (environmentId) { |
| 400 | 385 | targets.add(environmentId); |
| 401 | 386 | } |
| ... | ... | @@ -417,14 +402,14 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) { |
| 417 | 402 | }); |
| 418 | 403 | } |
| 419 | 404 | }, |
| 420 | [connectedEnvironmentIds, environmentId, persistServerSettings], | |
| 405 | [environmentId, persistServerSettings, sharedSettingsSyncTargetIds], | |
| 421 | 406 | ); |
| 422 | 407 | |
| 423 | 408 | return updateSettings; |
| 424 | 409 | } |
| 425 | 410 | |
| 426 | 411 | /** |
| 427 | * Connected environments whose shared settings differ from the primary's, | |
| 412 | * Shared-settings sync targets whose values differ from the primary's, | |
| 428 | 413 | * plus an action that writes the primary's values to all of them. Drift |
| 429 | 414 | * happens when an environment was offline during an edit or was changed by |
| 430 | 415 | * an older client. |
| ... | ... | @@ -437,7 +422,7 @@ export function useSharedSettingsSync() { |
| 437 | 422 | // must never push defaults over real values. Same for a primary too old to |
| 438 | 423 | // hold the shared keys: its decoded defaults are not a source of truth. |
| 439 | 424 | const primarySettings = |
| 440 | primaryEnvironment !== null && supportsSharedSettings(primaryEnvironment) | |
| 425 | primaryEnvironment !== null && supportsSharedSettingsSync(primaryEnvironment) | |
| 441 | 426 | ? (primaryEnvironment.serverConfig?.settings ?? null) |
| 442 | 427 | : null; |
| 443 | 428 | const { environments } = useEnvironments(); |
| ... | ... | @@ -454,7 +439,7 @@ export function useSharedSettingsSync() { |
| 454 | 439 | environments: environments.map((environment) => ({ |
| 455 | 440 | environmentId: environment.environmentId, |
| 456 | 441 | label: environment.label, |
| 457 | connected: supportsSharedSettings(environment), | |
| 442 | syncEligible: supportsSharedSettingsSync(environment), | |
| 458 | 443 | settings: environment.serverConfig?.settings ?? null, |
| 459 | 444 | })), |
| 460 | 445 | }), |
docs/internals/overview.md+4-3| ... | ... | @@ -90,9 +90,10 @@ A turn is complete when its session leaves `running` status, projected by |
| 90 | 90 | does not define turn end. |
| 91 | 91 | |
| 92 | 92 | Thread settlement is server-owned. Each server's own settings control PR and inactivity |
| 93 | settlement. Those keys are user preferences, so clients write them to every connected environment | |
| 94 | (`SHARED_SERVER_SETTING_KEYS` in `packages/client-runtime/src/state/sharedSettings.ts`) and warn | |
| 95 | when a connected environment drifts. | |
| 93 | settlement. Those keys are user preferences, so clients write them to every shared-settings sync | |
| 94 | target (`SHARED_SERVER_SETTING_KEYS` in `packages/client-runtime/src/state/sharedSettings.ts`) and | |
| 95 | warn when another target drifts. A target must have an active connection and advertise the | |
| 96 | `threadAutoSettlement` capability, which signals that the server can hold every shared key. | |
| 96 | 97 | [`ThreadSettlementReactor`][settlement] checks threads at startup, when those settings change, and |
| 97 | 98 | once per minute, including when no client is connected. It dispatches the guarded internal |
| 98 | 99 | `thread.auto-settle` command, which uses the existing settlement event lifecycle. Automatic |
docs/user/thread-sidebar.md+7-5| ... | ... | @@ -22,11 +22,13 @@ available, the inactivity rule still applies. A manual un-settle also keeps the |
| 22 | 22 | sorts by the moment you settled it. A thread that settled on its own sorts by its last message or |
| 23 | 23 | turn, not by when the server noticed it was inactive. |
| 24 | 24 | |
| 25 | Change these rules in **Settings > General**. The change is written to every environment you are | |
| 26 | connected to at that moment. An environment that is offline keeps its old value. When a connected | |
| 27 | environment holds a different value, **Settings > General** shows a warning that names it. Choose | |
| 28 | **Apply to all** to write your current values to every connected environment. The same applies to | |
| 29 | the new-thread workspace mode and the source control writing style. | |
| 25 | Change these rules in **Settings > General**. The change is written to every connected environment | |
| 26 | whose server supports shared settings. An environment that is offline or needs a server update | |
| 27 | keeps its old value and does not appear in mismatch warnings. When a connected environment whose | |
| 28 | server supports shared settings holds a different value, **Settings > General** shows a warning | |
| 29 | that names it. Choose **Apply to all** to write your current values to the environments named in | |
| 30 | the warning. The same applies to the new-thread workspace mode and the source control writing | |
| 31 | style. | |
| 30 | 32 | |
| 31 | 33 | A settings change affects future settlement and does not reopen a settled thread. Settings saved |
| 32 | 34 | by older clients on one device no longer control this behavior. |
packages/client-runtime/src/state/sharedSettings.test.ts+48-9| ... | ... | @@ -5,12 +5,36 @@ import { |
| 5 | 5 | findSharedSettingsMismatches, |
| 6 | 6 | pickSharedServerSettings, |
| 7 | 7 | splitSharedServerPatch, |
| 8 | supportsSharedSettingsSync, | |
| 8 | 9 | } from "./sharedSettings.ts"; |
| 9 | 10 | |
| 10 | 11 | const primaryId = EnvironmentId.make("env-primary"); |
| 11 | 12 | const laptopId = EnvironmentId.make("env-laptop"); |
| 12 | 13 | const boxId = EnvironmentId.make("env-box"); |
| 13 | 14 | |
| 15 | describe("supportsSharedSettingsSync", () => { | |
| 16 | it("accepts only connected servers that advertise the shared-settings capability", () => { | |
| 17 | expect( | |
| 18 | supportsSharedSettingsSync({ | |
| 19 | connection: { phase: "connected" }, | |
| 20 | serverConfig: { environment: { capabilities: { threadAutoSettlement: true } } }, | |
| 21 | }), | |
| 22 | ).toBe(true); | |
| 23 | expect( | |
| 24 | supportsSharedSettingsSync({ | |
| 25 | connection: { phase: "connected" }, | |
| 26 | serverConfig: { environment: { capabilities: {} } }, | |
| 27 | }), | |
| 28 | ).toBe(false); | |
| 29 | expect( | |
| 30 | supportsSharedSettingsSync({ | |
| 31 | connection: { phase: "reconnecting" }, | |
| 32 | serverConfig: { environment: { capabilities: { threadAutoSettlement: true } } }, | |
| 33 | }), | |
| 34 | ).toBe(false); | |
| 35 | }); | |
| 36 | }); | |
| 37 | ||
| 14 | 38 | describe("splitSharedServerPatch", () => { |
| 15 | 39 | it("routes preference keys to the shared patch and machine keys to the local patch", () => { |
| 16 | 40 | const { sharedPatch, localPatch } = splitSharedServerPatch({ |
| ... | ... | @@ -38,17 +62,27 @@ describe("pickSharedServerSettings", () => { |
| 38 | 62 | describe("findSharedSettingsMismatches", () => { |
| 39 | 63 | const primarySettings = { ...DEFAULT_SERVER_SETTINGS, sidebarAutoSettleAfterDays: 7 }; |
| 40 | 64 | |
| 41 | it("lists connected environments whose shared settings differ", () => { | |
| 65 | it("lists sync-eligible environments whose shared settings differ", () => { | |
| 42 | 66 | const mismatches = findSharedSettingsMismatches({ |
| 43 | 67 | primaryEnvironmentId: primaryId, |
| 44 | 68 | primarySettings, |
| 45 | 69 | environments: [ |
| 46 | { environmentId: primaryId, label: "Desktop", connected: true, settings: primarySettings }, | |
| 47 | { environmentId: laptopId, label: "Laptop", connected: true, settings: primarySettings }, | |
| 70 | { | |
| 71 | environmentId: primaryId, | |
| 72 | label: "Desktop", | |
| 73 | syncEligible: true, | |
| 74 | settings: primarySettings, | |
| 75 | }, | |
| 76 | { | |
| 77 | environmentId: laptopId, | |
| 78 | label: "Laptop", | |
| 79 | syncEligible: true, | |
| 80 | settings: primarySettings, | |
| 81 | }, | |
| 48 | 82 | { |
| 49 | 83 | environmentId: boxId, |
| 50 | 84 | label: "Remote Box", |
| 51 | connected: true, | |
| 85 | syncEligible: true, | |
| 52 | 86 | settings: DEFAULT_SERVER_SETTINGS, |
| 53 | 87 | }, |
| 54 | 88 | ], |
| ... | ... | @@ -64,7 +98,7 @@ describe("findSharedSettingsMismatches", () => { |
| 64 | 98 | { |
| 65 | 99 | environmentId: boxId, |
| 66 | 100 | label: "Remote Box", |
| 67 | connected: true, | |
| 101 | syncEligible: true, | |
| 68 | 102 | settings: { ...primarySettings, enableAgentBrowserAccess: false }, |
| 69 | 103 | }, |
| 70 | 104 | ], |
| ... | ... | @@ -74,7 +108,12 @@ describe("findSharedSettingsMismatches", () => { |
| 74 | 108 | |
| 75 | 109 | it("reports nothing until the primary environment's settings are loaded", () => { |
| 76 | 110 | const environments = [ |
| 77 | { environmentId: boxId, label: "Remote Box", connected: true, settings: primarySettings }, | |
| 111 | { | |
| 112 | environmentId: boxId, | |
| 113 | label: "Remote Box", | |
| 114 | syncEligible: true, | |
| 115 | settings: primarySettings, | |
| 116 | }, | |
| 78 | 117 | ]; |
| 79 | 118 | expect( |
| 80 | 119 | findSharedSettingsMismatches({ primaryEnvironmentId: null, primarySettings, environments }), |
| ... | ... | @@ -88,7 +127,7 @@ describe("findSharedSettingsMismatches", () => { |
| 88 | 127 | ).toEqual([]); |
| 89 | 128 | }); |
| 90 | 129 | |
| 91 | it("skips offline environments and environments without a loaded config", () => { | |
| 130 | it("skips ineligible environments and environments without a loaded config", () => { | |
| 92 | 131 | const mismatches = findSharedSettingsMismatches({ |
| 93 | 132 | primaryEnvironmentId: primaryId, |
| 94 | 133 | primarySettings, |
| ... | ... | @@ -96,10 +135,10 @@ describe("findSharedSettingsMismatches", () => { |
| 96 | 135 | { |
| 97 | 136 | environmentId: laptopId, |
| 98 | 137 | label: "Laptop", |
| 99 | connected: false, | |
| 138 | syncEligible: false, | |
| 100 | 139 | settings: DEFAULT_SERVER_SETTINGS, |
| 101 | 140 | }, |
| 102 | { environmentId: boxId, label: "Remote Box", connected: true, settings: null }, | |
| 141 | { environmentId: boxId, label: "Remote Box", syncEligible: true, settings: null }, | |
| 103 | 142 | ], |
| 104 | 143 | }); |
| 105 | 144 | expect(mismatches).toEqual([]); |
packages/client-runtime/src/state/sharedSettings.ts+36-10| ... | ... | @@ -4,14 +4,21 @@ |
| 4 | 4 | * Every server keeps its own `settings.json`, but some keys are user |
| 5 | 5 | * preferences that only live on the server because the server has to act on |
| 6 | 6 | * them (auto-settlement runs with no client attached). A user does not want |
| 7 | * those to differ per machine. Clients write these keys to every connected | |
| 8 | * environment, and warn when a connected environment still holds a different | |
| 9 | * value so the user can push their current value out. | |
| 7 | * those to differ per machine. Clients write these keys to every shared-settings | |
| 8 | * sync target, and warn when another target still holds a different value so | |
| 9 | * the user can push their current value out. | |
| 10 | 10 | */ |
| 11 | import type { EnvironmentId, ServerSettings, ServerSettingsPatch } from "@t3tools/contracts"; | |
| 11 | import type { | |
| 12 | EnvironmentId, | |
| 13 | ExecutionEnvironmentCapabilities, | |
| 14 | ServerSettings, | |
| 15 | ServerSettingsPatch, | |
| 16 | } from "@t3tools/contracts"; | |
| 12 | 17 | import * as Equal from "effect/Equal"; |
| 13 | 18 | import * as Struct from "effect/Struct"; |
| 14 | 19 | |
| 20 | import type { EnvironmentConnectionPhase } from "../connection/presentation.ts"; | |
| 21 | ||
| 15 | 22 | /** Server keys that hold a user preference rather than machine config. */ |
| 16 | 23 | export const SHARED_SERVER_SETTING_KEYS = [ |
| 17 | 24 | "sidebarAutoSettleAfterDays", |
| ... | ... | @@ -50,18 +57,37 @@ export function pickSharedServerSettings(settings: ServerSettings): ServerSettin |
| 50 | 57 | return Struct.pick(settings, SHARED_SERVER_SETTING_KEYS); |
| 51 | 58 | } |
| 52 | 59 | |
| 60 | /** | |
| 61 | * Whether an environment can participate in shared-settings sync right now. | |
| 62 | * Auto-settlement is the newest feature backed by a shared key, so a server | |
| 63 | * advertising `threadAutoSettlement` can hold every shared key. | |
| 64 | */ | |
| 65 | export function supportsSharedSettingsSync(environment: { | |
| 66 | readonly connection: { readonly phase: EnvironmentConnectionPhase }; | |
| 67 | readonly serverConfig: { | |
| 68 | readonly environment: { | |
| 69 | readonly capabilities: Pick<ExecutionEnvironmentCapabilities, "threadAutoSettlement">; | |
| 70 | }; | |
| 71 | } | null; | |
| 72 | }): boolean { | |
| 73 | return ( | |
| 74 | environment.connection.phase === "connected" && | |
| 75 | environment.serverConfig?.environment.capabilities.threadAutoSettlement === true | |
| 76 | ); | |
| 77 | } | |
| 78 | ||
| 53 | 79 | export interface SharedSettingsEnvironment { |
| 54 | 80 | readonly environmentId: EnvironmentId; |
| 55 | 81 | readonly label: string; |
| 56 | readonly connected: boolean; | |
| 82 | readonly syncEligible: boolean; | |
| 57 | 83 | readonly settings: ServerSettings | null; |
| 58 | 84 | } |
| 59 | 85 | |
| 60 | 86 | /** |
| 61 | * Connected environments whose shared settings differ from the primary | |
| 62 | * environment's. Offline environments are skipped: nothing can be read from | |
| 63 | * or written to them, and the warning would never clear. With no primary | |
| 64 | * settings loaded there is nothing to compare against, so nothing is | |
| 87 | * Shared-settings sync targets whose values differ from the primary | |
| 88 | * environment's. Other environments are skipped: nothing can be read from or | |
| 89 | * written to them, or their server cannot hold every shared key. With no | |
| 90 | * primary settings loaded there is nothing to compare against, so nothing is | |
| 65 | 91 | * reported. Callers must pass the real loaded settings, never a default |
| 66 | 92 | * fallback, or "apply to all" would push defaults over real values. |
| 67 | 93 | */ |
| ... | ... | @@ -77,7 +103,7 @@ export function findSharedSettingsMismatches(input: { |
| 77 | 103 | return input.environments.flatMap((environment) => { |
| 78 | 104 | if ( |
| 79 | 105 | environment.environmentId === input.primaryEnvironmentId || |
| 80 | !environment.connected || | |
| 106 | !environment.syncEligible || | |
| 81 | 107 | environment.settings === null |
| 82 | 108 | ) { |
| 83 | 109 | return []; |