@gphone/sdk
    Preparing search index...

    Interface Facets

    One key per facet function. The runtime object living behind this shape is the Proxy exported as facets from ../../current.ts.

    interface Facets {
        account: () => {
            bankBalance: Writable<number>;
            citizenid: Writable<string>;
            fetchBalance: () => Promise<void>;
            fetchCitizenId: () => Promise<string>;
            fetchPhoneNumber: () => Promise<string>;
            fetchTransactions: () => Promise<void>;
            myPhoneNumber: Writable<string>;
            transactions: Writable<Transaction[]>;
            transactionsLoaded: Writable<boolean>;
        };
        accounts: () => {
            blockAccount: (
                input: {
                    app: string;
                    blocked_account_id: number;
                    blocker_account_id: number;
                },
            ) => Promise<any>;
            createAccount: (
                input: { app: string; display_name?: string; handle: string },
            ) => Promise<Account>;
            followAccount: (
                input: {
                    app: string;
                    followee_account_id: number;
                    follower_account_id: number;
                },
            ) => Promise<any>;
            getAccounts: (
                query: { app: string; handle?: string; limit?: number },
            ) => Promise<{ rows: Account[] }>;
            getFollowers: (query: FollowListQuery) => Promise<FollowPage>;
            getFollowing: (query: FollowListQuery) => Promise<FollowPage>;
            getFollowStats: (
                input: { account_id: number; app: string; viewer_account_id?: number },
            ) => Promise<FollowStats>;
            getMyAccounts: (
                app: string,
            ) => Promise<{ limit: number; rows: Account[] }>;
            getReactionsFor: (
                target: ReactionTarget,
            ) => Promise<Record<number, ReactionSummary>>;
            reactToTarget: (
                payload: {
                    account_id: number;
                    app: string;
                    emoji: string;
                    target_id: number;
                    target_table: string;
                },
            ) => Promise<any>;
            searchAccounts: (
                query: AccountSearchQuery,
            ) => Promise<{ nextCursor: number | null; rows: Account[] }>;
            unblockAccount: (
                input: {
                    app: string;
                    blocked_account_id: number;
                    blocker_account_id: number;
                },
            ) => Promise<any>;
            unfollowAccount: (
                input: {
                    app: string;
                    followee_account_id: number;
                    follower_account_id: number;
                },
            ) => Promise<any>;
            unreactToTarget: (
                payload: {
                    account_id: number;
                    app: string;
                    emoji: string;
                    target_id: number;
                    target_table: string;
                },
            ) => Promise<any>;
            updateAccount: (input: { id: number } & Partial<Account>) => Promise<any>;
        };
        admin: () => {
            isAdmin: Writable<boolean>;
            refreshAdmin: () => Promise<void>;
        };
        appAction: (
            appId?: string,
        ) => {
            busy: Writable<boolean>;
            notify: (
                n: { message: string; title?: string; type: "error" | "success" },
            ) => string;
            run: (work: () => unknown, options?: AppActionOptions) => Promise<boolean>;
        };
        appEvents: (
            appId: string,
        ) => {
            clear: () => void;
            on: <T = Record<string, unknown>>(
                event: string,
                handler: (e: AppEvent<T>) => void,
            ) => () => void;
            onAny: (handler: (e: AppEvent) => void) => () => void;
        };
        appLevels: (
            config: AppLevelsConfig,
        ) => { back: () => void; release: () => void; get title(): string };
        appRegistry: () => {
            bundledAddOns: AppManifest[];
            getFirstBootTime: () => string;
            installFromCatalog: (
                entry: CatalogEntry,
            ) => Promise<{ manifest: AppManifest }>;
            registerAddOn: (manifest: AppManifest, source?: string) => void;
            registerApp: (manifest: AppManifest, component: AppComponent) => void;
            registryStore: {
                getAddOnSource: (appId: string) => Promise<string | undefined>;
                getComponent: (appId: string) => AppComponent | undefined;
                getManifest: (appId: string) => AppManifest | undefined;
                installFromCatalog: (
                    entry: CatalogEntry,
                ) => Promise<{ manifest: AppManifest }>;
                isInstalled: (appId: string) => boolean;
                isKnownApp: (appId: string) => boolean;
                loadComponent: (appId: string) => Promise<AppComponent | undefined>;
                registerAddOn: (manifest: AppManifest, source?: string) => void;
                registerApp: (manifest: AppManifest, component: AppComponent) => void;
                rehydrateSavedRemoteApps: () => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<AppManifest[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                unregisterApp: (appId: string) => void;
            };
            unregisterApp: (appId: string) => void;
        };
        appStorageBytes: (appId: string) => number;
        call: () => {
            answerCall: () => Promise<void>;
            callLog: Writable<PhoneCallLogEntry[]>;
            callStore: {
                answerCall: () => Promise<void>;
                endCall: () => Promise<void>;
                setIncoming: (number: string, name?: string) => void;
                setStatus: (status: CallStatus) => void;
                startCall: (number: string, name?: string) => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<CallState>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                toggleMute: () => Promise<void>;
                toggleSpeaker: () => Promise<void>;
            };
            endCall: () => Promise<void>;
            loadCallLog: () => Promise<void>;
            startCall: (number: string, name?: string) => Promise<void>;
            toggleSpeaker: () => Promise<void>;
        };
        camera: () => {
            isPreviewingPhoto: Writable<boolean>;
            isTakingPhoto: Writable<boolean>;
        };
        clearAppStorage: (appId: string) => void;
        clock: () => {
            formattedTime: Readable<string>;
            is24Hour: Writable<boolean>;
            time: Writable<TimeState>;
        };
        contacts: () => {
            addContact: (
                firstname: string,
                phone: string,
                lastname?: string,
                avatar?: string,
                favorite?: boolean,
            ) => Promise<Contact>;
            contactsStore: {
                add: (draft: TDraft) => Promise<Contact>;
                delete: (id: number) => Promise<void>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: Contact[]) => void;
                share: (
                    payload: Partial<Contact> & { name?: string; phone: string },
                ) => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<Contact[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: Contact) => Promise<void>;
            };
            favoriteContacts: Readable<Contact[]>;
            shareContact: (
                firstname: string,
                phone: string,
                lastname?: string,
            ) => Promise<void>;
        };
        deepLink: (appId: string, handle: () => boolean) => void;
        devTools: () => {
            devToolsUnlocked: Writable<boolean>;
            lock: () => void;
            unlock: () => void;
        };
        display: () => {
            displaySize: Writable<number>;
            displaySizeDefault: number;
            homeGridColumns: Writable<number>;
            homeGridColumnsDefault: number;
            homeGridColumnsMax: number;
            homeGridColumnsMin: number;
            homeGridRows: Writable<number>;
            homeGridRowsDefault: number;
            homeGridRowsMax: number;
            homeGridRowsMin: number;
            isSizeLimited: Readable<boolean>;
            phoneBox: Readable<{ height: number; width: number }>;
            phoneScale: Readable<number>;
            setDisplaySize: (size: number) => void;
            setHomeGridSize: (columns: number, rows: number) => void;
        };
        highscores: () => {
            getLeaderboard: (app: string) => Promise<LeaderboardEntry[]>;
            submitScore: (app: string, score: number) => Promise<void>;
        };
        keybinds: () => {
            bindings: Readable<Record<string, string>>;
            findConflict: (actionId: string, key: string) => KeybindAction | undefined;
            groups: Readable<KeybindGroup[]>;
            onKeybind: (
                actionId: string,
                handler: () => void,
                appId?: string,
            ) => () => void;
            resetBindings: () => void;
            setBinding: (actionId: string, key: string) => void;
        };
        lifecycle: (
            appId: string,
        ) => {
            consumeDeepLink: () => void;
            currentApp: Writable<RunningApp>;
            goHome: () => void;
            onBack: (handler: () => void) => () => void;
        };
        location: () => {
            setWaypoint: (x: number, y: number) => Promise<void>;
            shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
        };
        mail: () => {
            addReceivedMail: (newMail: Mail) => void;
            archiveMail: (id: number, archiveState?: boolean) => Promise<void>;
            deleteMail: (id: number) => Promise<void>;
            mailStore: {
                add: (draft: TDraft) => Promise<Mail>;
                addReceivedMail: (incoming: Mail) => void;
                archive: (id: number, archive?: boolean) => Promise<void>;
                delete: (id: number) => Promise<void>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                markAsRead: (id: number) => Promise<void>;
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: Mail[]) => void;
                subscribe: (
                    this: void,
                    run: Subscriber<Mail[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: Mail) => Promise<void>;
            };
            markAsRead: (id: number) => Promise<void>;
            unreadMailCount: Readable<number>;
        };
        marketplace: () => {
            feedStore: Writable<ListingPage>;
            loadFeed: () => Promise<void>;
            loadMine: () => Promise<void>;
            markSold: (id: number) => Promise<boolean>;
            mineStore: Writable<ListingPage>;
            postListing: (input: CreateListingInput) => Promise<Listing>;
            removeListing: (id: number) => Promise<boolean>;
            searchListings: (q: string) => Promise<ListingPage>;
            viewListing: (
                id: number,
            ) => Promise<
                Listing & { contactPhone: string | null; isOwn: boolean }
                | null,
            >;
        };
        media: () => {
            capturePhoto: (data: string) => Promise<MediaItem>;
            deletePhoto: (id: number) => Promise<void>;
            dropNearby: (mediaId: number) => Promise<{ count: number }>;
            media: {
                add: (draft: TDraft) => Promise<MediaItem>;
                delete: (id: number) => Promise<void>;
                dropNearby: (mediaId: number) => Promise<{ count: number }>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: MediaItem[]) => void;
                setWaypoint: (x: number, y: number) => Promise<void>;
                shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
                subscribe: (
                    this: void,
                    run: Subscriber<MediaItem[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: MediaItem) => Promise<void>;
            };
        };
        messages: () => {
            addReceivedMessage: (
                message: {
                    avatar?: string;
                    conversation_id?: number;
                    created_at?: string;
                    message?: string;
                    phone?: string;
                    reply_to_id?: number | null;
                    senderName?: string;
                },
            ) => void;
            conversationsStore: {
                activeConversationId: {
                    subscribe: (
                        this: void,
                        run: Subscriber<number | null>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                addReceivedMessage: (
                    incoming: {
                        avatar?: string;
                        conversation_id?: number;
                        created_at?: string;
                        message?: string;
                        phone?: string;
                        reply_to_id?: number | null;
                        senderName?: string;
                    },
                ) => void;
                archiveConversation: (
                    conversationId: number,
                    archive?: boolean,
                ) => Promise<void>;
                deleteConversation: (conversationId: number) => Promise<void>;
                loadConversations: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                loadMessages: (conversationId: number) => Promise<void>;
                markAsRead: (conversationId: number) => Promise<void>;
                messages: {
                    subscribe: (
                        this: void,
                        run: Subscriber<Record<number, UIMessage[]>>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                renameConversation: (
                    conversationId: number,
                    name: string,
                ) => Promise<void>;
                sendMessage: (
                    conversationId: number,
                    message: string,
                    attachments?: { attachment?: string; photo_id: number }[],
                    replyToId?: number | null,
                ) => Promise<Message | null>;
                setActiveConversationId: (id: number | null) => void;
                startConversation: (
                    phone: string,
                    isGroup?: boolean,
                ) => Promise<UIConversation | null>;
                subscribe: (
                    this: void,
                    run: Subscriber<UIConversation[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            sendMessage: (
                conversationId: number,
                text: string,
            ) => Promise<Message | null>;
            startText: (phone: string) => void;
            unreadMessagesCount: Readable<number>;
        };
        navigation: () => {
            closePhone: () => void;
            currentApp: Writable<RunningApp>;
            goHome: () => void;
            openApp: (appName: string, props?: Record<string, unknown>) => void;
        };
        notifications: (
            appId?: string,
        ) => {
            clear: (ids: number[]) => Promise<void>;
            clearAll: (targetAppId?: string) => Promise<void>;
            load: () => Promise<void>;
            loaded: Writable<boolean>;
            markRead: (ids: number[]) => Promise<void>;
            notificationsStore: Readable<NotificationItem[]>;
            totalUnread: Readable<number>;
            unreadCount: Readable<number>;
        };
        notificationSettings: () => {
            badgesEnabled: Writable<boolean>;
            notificationSoundEnabled: Writable<boolean>;
            toastsEnabled: Writable<boolean>;
        };
        onAppForeground: (appId: string, handler: () => void) => () => void;
        onAppUnmount: (handler: () => void) => void;
        persisted: <T>(
            appId: string,
            key: string,
            initial: T,
            options?: PersistedOptions<T>,
        ) => Writable<T>;
        phoneNotification: () => {
            dismissNotification: (id: string) => void;
            sendNotification: (options: SendNotificationOptions) => string;
            toast: {
                archive: (id: string) => Promise<void>;
                clear: () => void;
                dismiss: (id: string) => void;
                pauseDismiss: (id: string) => void;
                resumeDismiss: (id: string, delay?: number) => void;
                show: (options: Partial<ToastMessage> & { message: string }) => string;
                showCall: (
                    options: {
                        name?: string;
                        number: string;
                        onAccept: () => void | Promise<void>;
                        onDecline?: () => void | Promise<void>;
                        onExpire?: () => void | Promise<void>;
                    },
                ) => string;
                showContactShare: (
                    options: {
                        avatar?: string;
                        name: string;
                        onAccept: () => void | Promise<void>;
                        onClick?: () => void | Promise<void>;
                        onDecline?: () => void | Promise<void>;
                        phone: string;
                    },
                ) => string;
                showIncomingMessage: (
                    options: {
                        avatar?: string;
                        message: string;
                        onClick?: () => void;
                        onReply: (replyText: string) => void | Promise<void>;
                        sender: string;
                    },
                ) => string;
                showMail: (
                    options: { onClick?: () => void; sender: string; subject: string },
                ) => string;
                subscribe: (
                    this: void,
                    run: Subscriber<ToastMessage[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
        };
        report: () => { submit: (input: SubmitReportInput) => Promise<void> };
        reports: () => {
            loadPendingReports: () => Promise<void>;
            loadReportHistory: () => Promise<void>;
            pendingReportCount: Readable<number>;
            pendingReports: Writable<Report[]>;
            reopenReport: (id: number) => Promise<void>;
            resolvedReports: Writable<Report[]>;
            resolveReport: (
                id: number,
                action: "moderate" | "dismiss",
            ) => Promise<void>;
        };
        service: (
            serviceId: string,
        ) => {
            call: <T = unknown>(
                action: string,
                data?: unknown,
                defaultValue?: T,
            ) => Promise<T>;
            id: string;
        };
        sound: () => { play: (effect: SoundEffect) => void };
        storage: (
            appId: string,
        ) => {
            clear: () => void;
            getItem: <T = unknown>(key: string, defaultValue?: T) => T | null;
            markUnsynced: (key: string) => void;
            removeItem: (key: string) => void;
            setItem: <T = unknown>(key: string, value: T) => void;
        };
        systemHardware: () => {
            bluetoothEnabled: Writable<boolean>;
            cellServiceEnabled: Writable<boolean>;
            charge: Writable<number>;
            isBluetoothDiscoverable: Readable<boolean>;
            setSignal: (level: number) => void;
            setVolume: (val: number) => void;
            setVolumeStep: (percent: number) => void;
            signalLevel: Writable<number>;
            soundMuted: Writable<boolean>;
            soundVolume: Writable<number>;
            toggleBluetooth: () => void;
            toggleCellService: () => void;
            toggleMute: () => void;
            volumeStep: Writable<number>;
            volumeStepChoices: readonly [1, 2, 5, 10, 20];
        };
        theme: () => {
            defaultTheme: ThemeState;
            isLightMode: Readable<boolean>;
            resetTheme: () => void;
            sanitizeSeed: (value: unknown) => string;
            schemeStore: Readable<M3Tokens>;
            seedFromRgbString: (value: string) => string | null;
            setThemeMode: (mode: ThemeMode) => void;
            setThemeSeed: (seed: string) => void;
            themeStore: Writable<ThemeState>;
        };
        timer: () => {
            after: (ms: number, handler: () => void) => CancelTimer;
            clearAll: () => void;
            every: (ms: number, handler: () => void) => CancelTimer;
        };
        wallpaper: () => {
            activeSeed: Readable<string>;
            backgroundForSeed: (seed: string, mode: ThemeMode) => string;
            defaultWallpaper: WallpaperState;
            presets: readonly WallpaperPreset[];
            resetWallpaper: () => void;
            seedFromImage: (source: string) => Promise<string | null>;
            setPresetWallpaper: (preset: WallpaperPreset) => void;
            setWallpaperImage: (image: string, seed?: string) => void;
            setWallpaperSeed: (seed: string) => void;
            wallpaperBackground: Readable<string>;
            wallpaperNeedsContrast: Readable<boolean>;
            wallpaperStore: Writable<WallpaperState>;
        };
    }
    Index
    account: () => {
        bankBalance: Writable<number>;
        citizenid: Writable<string>;
        fetchBalance: () => Promise<void>;
        fetchCitizenId: () => Promise<string>;
        fetchPhoneNumber: () => Promise<string>;
        fetchTransactions: () => Promise<void>;
        myPhoneNumber: Writable<string>;
        transactions: Writable<Transaction[]>;
        transactionsLoaded: Writable<boolean>;
    }

    Type Declaration

      • (): {
            bankBalance: Writable<number>;
            citizenid: Writable<string>;
            fetchBalance: () => Promise<void>;
            fetchCitizenId: () => Promise<string>;
            fetchPhoneNumber: () => Promise<string>;
            fetchTransactions: () => Promise<void>;
            myPhoneNumber: Writable<string>;
            transactions: Writable<Transaction[]>;
            transactionsLoaded: Writable<boolean>;
        }
      • OS Service Hook for account state, bank balance, transactions, & phone number.

        Returns {
            bankBalance: Writable<number>;
            citizenid: Writable<string>;
            fetchBalance: () => Promise<void>;
            fetchCitizenId: () => Promise<string>;
            fetchPhoneNumber: () => Promise<string>;
            fetchTransactions: () => Promise<void>;
            myPhoneNumber: Writable<string>;
            transactions: Writable<Transaction[]>;
            transactionsLoaded: Writable<boolean>;
        }

        • bankBalance: Writable<number>
        • citizenid: Writable<string>
        • fetchBalance: () => Promise<void>
        • fetchCitizenId: () => Promise<string>
        • fetchPhoneNumber: () => Promise<string>
        • fetchTransactions: () => Promise<void>
        • myPhoneNumber: Writable<string>
        • transactions: Writable<Transaction[]>
        • transactionsLoaded: Writable<boolean>

          False until the first transaction fetch has come back.

          Same signal createCrudStore exposes, for the same reason: an empty list is not the same statement as "this account has no transactions", and Bank used to make the second one while still waiting for the first.

    accounts: () => {
        blockAccount: (
            input: {
                app: string;
                blocked_account_id: number;
                blocker_account_id: number;
            },
        ) => Promise<any>;
        createAccount: (
            input: { app: string; display_name?: string; handle: string },
        ) => Promise<Account>;
        followAccount: (
            input: {
                app: string;
                followee_account_id: number;
                follower_account_id: number;
            },
        ) => Promise<any>;
        getAccounts: (
            query: { app: string; handle?: string; limit?: number },
        ) => Promise<{ rows: Account[] }>;
        getFollowers: (query: FollowListQuery) => Promise<FollowPage>;
        getFollowing: (query: FollowListQuery) => Promise<FollowPage>;
        getFollowStats: (
            input: { account_id: number; app: string; viewer_account_id?: number },
        ) => Promise<FollowStats>;
        getMyAccounts: (app: string) => Promise<{ limit: number; rows: Account[] }>;
        getReactionsFor: (
            target: ReactionTarget,
        ) => Promise<Record<number, ReactionSummary>>;
        reactToTarget: (
            payload: {
                account_id: number;
                app: string;
                emoji: string;
                target_id: number;
                target_table: string;
            },
        ) => Promise<any>;
        searchAccounts: (
            query: AccountSearchQuery,
        ) => Promise<{ nextCursor: number | null; rows: Account[] }>;
        unblockAccount: (
            input: {
                app: string;
                blocked_account_id: number;
                blocker_account_id: number;
            },
        ) => Promise<any>;
        unfollowAccount: (
            input: {
                app: string;
                followee_account_id: number;
                follower_account_id: number;
            },
        ) => Promise<any>;
        unreactToTarget: (
            payload: {
                account_id: number;
                app: string;
                emoji: string;
                target_id: number;
                target_table: string;
            },
        ) => Promise<any>;
        updateAccount: (input: { id: number } & Partial<Account>) => Promise<any>;
    }

    Type Declaration

      • (): {
            blockAccount: (
                input: {
                    app: string;
                    blocked_account_id: number;
                    blocker_account_id: number;
                },
            ) => Promise<any>;
            createAccount: (
                input: { app: string; display_name?: string; handle: string },
            ) => Promise<Account>;
            followAccount: (
                input: {
                    app: string;
                    followee_account_id: number;
                    follower_account_id: number;
                },
            ) => Promise<any>;
            getAccounts: (
                query: { app: string; handle?: string; limit?: number },
            ) => Promise<{ rows: Account[] }>;
            getFollowers: (query: FollowListQuery) => Promise<FollowPage>;
            getFollowing: (query: FollowListQuery) => Promise<FollowPage>;
            getFollowStats: (
                input: { account_id: number; app: string; viewer_account_id?: number },
            ) => Promise<FollowStats>;
            getMyAccounts: (app: string) => Promise<{ limit: number; rows: Account[] }>;
            getReactionsFor: (
                target: ReactionTarget,
            ) => Promise<Record<number, ReactionSummary>>;
            reactToTarget: (
                payload: {
                    account_id: number;
                    app: string;
                    emoji: string;
                    target_id: number;
                    target_table: string;
                },
            ) => Promise<any>;
            searchAccounts: (
                query: AccountSearchQuery,
            ) => Promise<{ nextCursor: number | null; rows: Account[] }>;
            unblockAccount: (
                input: {
                    app: string;
                    blocked_account_id: number;
                    blocker_account_id: number;
                },
            ) => Promise<any>;
            unfollowAccount: (
                input: {
                    app: string;
                    followee_account_id: number;
                    follower_account_id: number;
                },
            ) => Promise<any>;
            unreactToTarget: (
                payload: {
                    account_id: number;
                    app: string;
                    emoji: string;
                    target_id: number;
                    target_table: string;
                },
            ) => Promise<any>;
            updateAccount: (input: { id: number } & Partial<Account>) => Promise<any>;
        }
      • Implementation of the useAccounts facet — see the useAccounts hook doc for the usage contract.

        Returns {
            blockAccount: (
                input: {
                    app: string;
                    blocked_account_id: number;
                    blocker_account_id: number;
                },
            ) => Promise<any>;
            createAccount: (
                input: { app: string; display_name?: string; handle: string },
            ) => Promise<Account>;
            followAccount: (
                input: {
                    app: string;
                    followee_account_id: number;
                    follower_account_id: number;
                },
            ) => Promise<any>;
            getAccounts: (
                query: { app: string; handle?: string; limit?: number },
            ) => Promise<{ rows: Account[] }>;
            getFollowers: (query: FollowListQuery) => Promise<FollowPage>;
            getFollowing: (query: FollowListQuery) => Promise<FollowPage>;
            getFollowStats: (
                input: { account_id: number; app: string; viewer_account_id?: number },
            ) => Promise<FollowStats>;
            getMyAccounts: (app: string) => Promise<{ limit: number; rows: Account[] }>;
            getReactionsFor: (
                target: ReactionTarget,
            ) => Promise<Record<number, ReactionSummary>>;
            reactToTarget: (
                payload: {
                    account_id: number;
                    app: string;
                    emoji: string;
                    target_id: number;
                    target_table: string;
                },
            ) => Promise<any>;
            searchAccounts: (
                query: AccountSearchQuery,
            ) => Promise<{ nextCursor: number | null; rows: Account[] }>;
            unblockAccount: (
                input: {
                    app: string;
                    blocked_account_id: number;
                    blocker_account_id: number;
                },
            ) => Promise<any>;
            unfollowAccount: (
                input: {
                    app: string;
                    followee_account_id: number;
                    follower_account_id: number;
                },
            ) => Promise<any>;
            unreactToTarget: (
                payload: {
                    account_id: number;
                    app: string;
                    emoji: string;
                    target_id: number;
                    target_table: string;
                },
            ) => Promise<any>;
            updateAccount: (input: { id: number } & Partial<Account>) => Promise<any>;
        }

    admin: () => { isAdmin: Writable<boolean>; refreshAdmin: () => Promise<void> }

    Type Declaration

      • (): { isAdmin: Writable<boolean>; refreshAdmin: () => Promise<void> }
      • OS Service Hook for the player's admin status.

        Read-only as far as apps are concerned: this reflects what the server said, and nothing an app does can change it. Gating a screen on it hides a button, never a capability — the privileged action itself is checked again server-side.

        Returns { isAdmin: Writable<boolean>; refreshAdmin: () => Promise<void> }

        • isAdmin: Writable<boolean>

          Whether the player holds an admin ace, as decided by the server.

          One store rather than each screen asking for itself. Settings already fetched this privately to gate Developer Tools, and the home screen needs the same answer to decide whether the Administration app exists — two copies of one fact drift, and the whole point of asking the server is that there is a single authority.

          It decides what the UI shows and nothing more. Every privileged action is checked again server-side, because a NUI request is not proof of intent (AGENTS.md §2.9).

          A plain browser has no ace list and no server to ask, so it stands in as allowed.

        • refreshAdmin: () => Promise<void>

          Ask once per session. Safe to call from anywhere that needs the answer.

    appAction: (
        appId?: string,
    ) => {
        busy: Writable<boolean>;
        notify: (
            n: { message: string; title?: string; type: "error" | "success" },
        ) => string;
        run: (work: () => unknown, options?: AppActionOptions) => Promise<boolean>;
    }

    Type Declaration

      • (
            appId?: string,
        ): {
            busy: Writable<boolean>;
            notify: (
                n: { message: string; title?: string; type: "error" | "success" },
            ) => string;
            run: (work: () => unknown, options?: AppActionOptions) => Promise<boolean>;
        }
      • Implementation of the useAppAction facet — see the useAppAction hook doc for the usage contract.

        Parameters

        • OptionalappId: string

        Returns {
            busy: Writable<boolean>;
            notify: (
                n: { message: string; title?: string; type: "error" | "success" },
            ) => string;
            run: (work: () => unknown, options?: AppActionOptions) => Promise<boolean>;
        }

        • busy: Writable<boolean>
        • notify: (n: { message: string; title?: string; type: "error" | "success" }) => string

          The toast half of run, for a caller whose work cannot cross a process boundary (GPHONE-16 step 4).

        • run: (work: () => unknown, options?: AppActionOptions) => Promise<boolean>
    appEvents: (
        appId: string,
    ) => {
        clear: () => void;
        on: <T = Record<string, unknown>>(
            event: string,
            handler: (e: AppEvent<T>) => void,
        ) => () => void;
        onAny: (handler: (e: AppEvent) => void) => () => void;
    }

    Type Declaration

      • (
            appId: string,
        ): {
            clear: () => void;
            on: <T = Record<string, unknown>>(
                event: string,
                handler: (e: AppEvent<T>) => void,
            ) => () => void;
            onAny: (handler: (e: AppEvent) => void) => () => void;
        }
      • Implementation of the useAppEvents facet — see the useAppEvents hook doc for the usage contract.

        Parameters

        • appId: string

        Returns {
            clear: () => void;
            on: <T = Record<string, unknown>>(
                event: string,
                handler: (e: AppEvent<T>) => void,
            ) => () => void;
            onAny: (handler: (e: AppEvent) => void) => () => void;
        }

        • clear: () => void

          Drop anything buffered, once a fetch has made it redundant.

        • on: <T = Record<string, unknown>>(
              event: string,
              handler: (e: AppEvent<T>) => void,
          ) => () => void

          T is an assertion, not a check. The bus guarantees payload is a plain object and nothing more; narrow it yourself.

        • onAny: (handler: (e: AppEvent) => void) => () => void
    appLevels: (
        config: AppLevelsConfig,
    ) => { back: () => void; release: () => void; get title(): string }

    Type Declaration

      • (
            config: AppLevelsConfig,
        ): { back: () => void; release: () => void; get title(): string }
      • Implementation of the useAppLevels facet — see the useAppLevels hook doc for the usage contract (the two-things-have-to-happen and appId-stops-Back rules).

        Parameters

        Returns { back: () => void; release: () => void; get title(): string }

        • back: () => void
        • release: () => void
        • get title(): string

          The deepest open level's title, falling back to the app's own.

    appRegistry: () => {
        bundledAddOns: AppManifest[];
        getFirstBootTime: () => string;
        installFromCatalog: (
            entry: CatalogEntry,
        ) => Promise<{ manifest: AppManifest }>;
        registerAddOn: (manifest: AppManifest, source?: string) => void;
        registerApp: (manifest: AppManifest, component: AppComponent) => void;
        registryStore: {
            getAddOnSource: (appId: string) => Promise<string | undefined>;
            getComponent: (appId: string) => AppComponent | undefined;
            getManifest: (appId: string) => AppManifest | undefined;
            installFromCatalog: (
                entry: CatalogEntry,
            ) => Promise<{ manifest: AppManifest }>;
            isInstalled: (appId: string) => boolean;
            isKnownApp: (appId: string) => boolean;
            loadComponent: (appId: string) => Promise<AppComponent | undefined>;
            registerAddOn: (manifest: AppManifest, source?: string) => void;
            registerApp: (manifest: AppManifest, component: AppComponent) => void;
            rehydrateSavedRemoteApps: () => Promise<void>;
            subscribe: (
                this: void,
                run: Subscriber<AppManifest[]>,
                invalidate?: () => void,
            ) => Unsubscriber;
            unregisterApp: (appId: string) => void;
        };
        unregisterApp: (appId: string) => void;
    }

    Type Declaration

      • (): {
            bundledAddOns: AppManifest[];
            getFirstBootTime: () => string;
            installFromCatalog: (
                entry: CatalogEntry,
            ) => Promise<{ manifest: AppManifest }>;
            registerAddOn: (manifest: AppManifest, source?: string) => void;
            registerApp: (manifest: AppManifest, component: AppComponent) => void;
            registryStore: {
                getAddOnSource: (appId: string) => Promise<string | undefined>;
                getComponent: (appId: string) => AppComponent | undefined;
                getManifest: (appId: string) => AppManifest | undefined;
                installFromCatalog: (
                    entry: CatalogEntry,
                ) => Promise<{ manifest: AppManifest }>;
                isInstalled: (appId: string) => boolean;
                isKnownApp: (appId: string) => boolean;
                loadComponent: (appId: string) => Promise<AppComponent | undefined>;
                registerAddOn: (manifest: AppManifest, source?: string) => void;
                registerApp: (manifest: AppManifest, component: AppComponent) => void;
                rehydrateSavedRemoteApps: () => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<AppManifest[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                unregisterApp: (appId: string) => void;
            };
            unregisterApp: (appId: string) => void;
        }
      • OS Service Hook for dynamic app registry & remote app installation.

        Returns {
            bundledAddOns: AppManifest[];
            getFirstBootTime: () => string;
            installFromCatalog: (
                entry: CatalogEntry,
            ) => Promise<{ manifest: AppManifest }>;
            registerAddOn: (manifest: AppManifest, source?: string) => void;
            registerApp: (manifest: AppManifest, component: AppComponent) => void;
            registryStore: {
                getAddOnSource: (appId: string) => Promise<string | undefined>;
                getComponent: (appId: string) => AppComponent | undefined;
                getManifest: (appId: string) => AppManifest | undefined;
                installFromCatalog: (
                    entry: CatalogEntry,
                ) => Promise<{ manifest: AppManifest }>;
                isInstalled: (appId: string) => boolean;
                isKnownApp: (appId: string) => boolean;
                loadComponent: (appId: string) => Promise<AppComponent | undefined>;
                registerAddOn: (manifest: AppManifest, source?: string) => void;
                registerApp: (manifest: AppManifest, component: AppComponent) => void;
                rehydrateSavedRemoteApps: () => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<AppManifest[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                unregisterApp: (appId: string) => void;
            };
            unregisterApp: (appId: string) => void;
        }

        • bundledAddOns: AppManifest[]

          Add-ons this repo ships uninstalled — what the Store has to offer beyond remotes.

        • getFirstBootTime: () => string
        • installFromCatalog: (entry: CatalogEntry) => Promise<{ manifest: AppManifest }>
        • registerAddOn: (manifest: AppManifest, source?: string) => void
        • registerApp: (manifest: AppManifest, component: AppComponent) => void
        • registryStore: {
              getAddOnSource: (appId: string) => Promise<string | undefined>;
              getComponent: (appId: string) => AppComponent | undefined;
              getManifest: (appId: string) => AppManifest | undefined;
              installFromCatalog: (
                  entry: CatalogEntry,
              ) => Promise<{ manifest: AppManifest }>;
              isInstalled: (appId: string) => boolean;
              isKnownApp: (appId: string) => boolean;
              loadComponent: (appId: string) => Promise<AppComponent | undefined>;
              registerAddOn: (manifest: AppManifest, source?: string) => void;
              registerApp: (manifest: AppManifest, component: AppComponent) => void;
              rehydrateSavedRemoteApps: () => Promise<void>;
              subscribe: (
                  this: void,
                  run: Subscriber<AppManifest[]>,
                  invalidate?: () => void,
              ) => Unsubscriber;
              unregisterApp: (appId: string) => void;
          }
          • getAddOnSource: (appId: string) => Promise<string | undefined>

            Fetch an add-on's bundle text, never its component — the sandboxed iframe transport runs it, this registry only ever hands over bytes. Idempotent, same as loadComponent.

          • getComponent: (appId: string) => AppComponent | undefined
          • getManifest: (appId: string) => AppManifest | undefined

            The manifest for an app the shell can render — installed, or a bundled add-on.

            The shell holds app ids; anything shown to a player needs the manifest's name. Without this the error boundary rendered the id rather than the manifest's name, which are not the same string.

            The addOns fallback exists because installed and renderable are not the same set for a core: false app. isKnownApp already counts a bundled add-on as known whether or not it has been installed, so openApp('notes') from a ?app= deep link legitimately makes it the current app — but Shell.svelte renders through {#if manifest && ...}, so an installed-only lookup left that path on a permanent spinner. Opening an uninstalled add-on straight from a deep link is by design (it is how the dev harness reaches one), so the manifest has to resolve for it. It is still a fact about the build, not the install: addOns is written once at startup from the manifest glob and nothing that is not in this bundle can appear in it.

          • installFromCatalog: (entry: CatalogEntry) => Promise<{ manifest: AppManifest }>

            The only way a remote app is ever installed. Builds the manifest from entry — never from anything the fetched bundle itself claims to be — after the bundle's bytes are hash-verified against entry.sha256. See installVerified below.

          • isInstalled: (appId: string) => boolean

            Whether the app has actually been installed — the question getManifest used to be asked in place of, and stopped being able to answer.

            getManifest resolves a bundled add-on that has never been installed (see its own comment), which is right for rendering one opened by a deep link and wrong for any gate that means "the player has this app". nuiMessages' appEvent is exactly such a gate: it reads the manifest's permissions to decide whether a pushed toast is allowed, and with the fallback in place a never-installed Blabber would have started raising toasts. Installed-ness is its own fact, so it gets its own question.

          • isKnownApp: (appId: string) => boolean

            Whether the app exists, regardless of whether its chunk has arrived.

          • loadComponent: (appId: string) => Promise<AppComponent | undefined>

            Fetch an app's component. Idempotent, and the only thing that imports app code.

          • registerAddOn: (manifest: AppManifest, source?: string) => void

            Register a manifest with its bundle as source text, never executed here.

            source given explicitly — a catalog install (installVerified, already hash-verified) or a dev-registered add-on — is stashed as-is. Omitted, manifest.id must be one of this build's own bundled add-ons (addOnIds); its text is left for getAddOnSource to fetch lazily, on first open, rather than eagerly here.

          • registerApp: (manifest: AppManifest, component: AppComponent) => void

            Register a manifest with its component already loaded — the in-process path.

            Stays for core apps, which always ran in-process. For a core: false manifest this is dev-only: error_boundary.spec.ts registers runtime crash fixtures through it, and nothing shipped is meant to reach it — a real add-on registers through registerAddOn below, as source text, and runs sandboxed. Blocked outside import.meta.env.DEV so a production build can't be handed a live component for an app that never went through the sandboxed transport.

          • rehydrateSavedRemoteApps: () => Promise<void>
          • subscribe: (
                this: void,
                run: Subscriber<AppManifest[]>,
                invalidate?: () => void,
            ) => Unsubscriber
          • unregisterApp: (appId: string) => void
        • unregisterApp: (appId: string) => void
    appStorageBytes: (appId: string) => number

    Type Declaration

      • (appId: string): number
      • Implementation of the appStorageBytes facet — see the appStorageBytes hook doc for the usage contract. Keys are counted alongside values here: both occupy the quota.

        Parameters

        • appId: string

        Returns number

    call: () => {
        answerCall: () => Promise<void>;
        callLog: Writable<PhoneCallLogEntry[]>;
        callStore: {
            answerCall: () => Promise<void>;
            endCall: () => Promise<void>;
            setIncoming: (number: string, name?: string) => void;
            setStatus: (status: CallStatus) => void;
            startCall: (number: string, name?: string) => Promise<void>;
            subscribe: (
                this: void,
                run: Subscriber<CallState>,
                invalidate?: () => void,
            ) => Unsubscriber;
            toggleMute: () => Promise<void>;
            toggleSpeaker: () => Promise<void>;
        };
        endCall: () => Promise<void>;
        loadCallLog: () => Promise<void>;
        startCall: (number: string, name?: string) => Promise<void>;
        toggleSpeaker: () => Promise<void>;
    }

    Type Declaration

      • (): {
            answerCall: () => Promise<void>;
            callLog: Writable<PhoneCallLogEntry[]>;
            callStore: {
                answerCall: () => Promise<void>;
                endCall: () => Promise<void>;
                setIncoming: (number: string, name?: string) => void;
                setStatus: (status: CallStatus) => void;
                startCall: (number: string, name?: string) => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<CallState>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                toggleMute: () => Promise<void>;
                toggleSpeaker: () => Promise<void>;
            };
            endCall: () => Promise<void>;
            loadCallLog: () => Promise<void>;
            startCall: (number: string, name?: string) => Promise<void>;
            toggleSpeaker: () => Promise<void>;
        }
      • OS Service Hook for active phone call management, plus the player's call history.

        Returns {
            answerCall: () => Promise<void>;
            callLog: Writable<PhoneCallLogEntry[]>;
            callStore: {
                answerCall: () => Promise<void>;
                endCall: () => Promise<void>;
                setIncoming: (number: string, name?: string) => void;
                setStatus: (status: CallStatus) => void;
                startCall: (number: string, name?: string) => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<CallState>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                toggleMute: () => Promise<void>;
                toggleSpeaker: () => Promise<void>;
            };
            endCall: () => Promise<void>;
            loadCallLog: () => Promise<void>;
            startCall: (number: string, name?: string) => Promise<void>;
            toggleSpeaker: () => Promise<void>;
        }

        • answerCall: () => Promise<void>
        • callLog: Writable<PhoneCallLogEntry[]>

          The signed-in player's call history, newest first.

        • callStore: {
              answerCall: () => Promise<void>;
              endCall: () => Promise<void>;
              setIncoming: (number: string, name?: string) => void;
              setStatus: (status: CallStatus) => void;
              startCall: (number: string, name?: string) => Promise<void>;
              subscribe: (
                  this: void,
                  run: Subscriber<CallState>,
                  invalidate?: () => void,
              ) => Unsubscriber;
              toggleMute: () => Promise<void>;
              toggleSpeaker: () => Promise<void>;
          }
          • answerCall: () => Promise<void>
          • endCall: () => Promise<void>
          • setIncoming: (number: string, name?: string) => void
          • setStatus: (status: CallStatus) => void
          • startCall: (number: string, name?: string) => Promise<void>
          • subscribe: (
                this: void,
                run: Subscriber<CallState>,
                invalidate?: () => void,
            ) => Unsubscriber
          • toggleMute: () => Promise<void>
          • toggleSpeaker: () => Promise<void>
        • endCall: () => Promise<void>
        • loadCallLog: () => Promise<void>
        • startCall: (number: string, name?: string) => Promise<void>
        • toggleSpeaker: () => Promise<void>
    camera: () => {
        isPreviewingPhoto: Writable<boolean>;
        isTakingPhoto: Writable<boolean>;
    }

    Type Declaration

      • (): { isPreviewingPhoto: Writable<boolean>; isTakingPhoto: Writable<boolean> }
      • The camera hardware — whether a capture is in flight, and whether the shot just taken is being previewed.

        The gallery half moved to useMedia. One hook covering both meant an app that only wanted to show pictures had to go through something called "camera", and the Photos app did exactly that.

        Returns { isPreviewingPhoto: Writable<boolean>; isTakingPhoto: Writable<boolean> }

    clearAppStorage: (appId: string) => void

    Type Declaration

      • (appId: string): void
      • Implementation of the clearAppStorage facet — see the clearAppStorage hook doc for the usage contract.

        Live persisted stores are reset alongside the keys — see persistedResets above for why the sweep alone leaves the app looking untouched.

        Parameters

        • appId: string

        Returns void

    clock: () => {
        formattedTime: Readable<string>;
        is24Hour: Writable<boolean>;
        time: Writable<TimeState>;
    }

    Type Declaration

      • (): {
            formattedTime: Readable<string>;
            is24Hour: Writable<boolean>;
            time: Writable<TimeState>;
        }
      • The phone's clock, and how it is displayed.

        Split out of useSystemHardware, which had grown to mean "anything the shell owns". A 12-versus-24-hour preference is not hardware — it is a locale setting that happens to live next to the clock, and an app asking for the time should not have to reach through battery and signal to find it.

        Returns {
            formattedTime: Readable<string>;
            is24Hour: Writable<boolean>;
            time: Writable<TimeState>;
        }

        • formattedTime: Readable<string>

          The time already rendered in the player's chosen form.

          Exposed so nothing re-implements the 12/24 branch. The status bar and the Display preview both show a clock, and two formatters would be one preference with two answers.

        • is24Hour: Writable<boolean>

          Whether to render it in 24-hour form. Writable: Settings toggles it.

        • time: Writable<TimeState>

          The current time, updated by the shell.

    contacts: () => {
        addContact: (
            firstname: string,
            phone: string,
            lastname?: string,
            avatar?: string,
            favorite?: boolean,
        ) => Promise<Contact>;
        contactsStore: {
            add: (draft: TDraft) => Promise<Contact>;
            delete: (id: number) => Promise<void>;
            load: () => Promise<void>;
            loaded: {
                subscribe: (
                    this: void,
                    run: Subscriber<boolean>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            patch: (id: number, changes: Partial<T>) => void;
            set: (rows: Contact[]) => void;
            share: (
                payload: Partial<Contact> & { name?: string; phone: string },
            ) => Promise<void>;
            subscribe: (
                this: void,
                run: Subscriber<Contact[]>,
                invalidate?: () => void,
            ) => Unsubscriber;
            update: (row: Contact) => Promise<void>;
        };
        favoriteContacts: Readable<Contact[]>;
        shareContact: (
            firstname: string,
            phone: string,
            lastname?: string,
        ) => Promise<void>;
    }

    Type Declaration

      • (): {
            addContact: (
                firstname: string,
                phone: string,
                lastname?: string,
                avatar?: string,
                favorite?: boolean,
            ) => Promise<Contact>;
            contactsStore: {
                add: (draft: TDraft) => Promise<Contact>;
                delete: (id: number) => Promise<void>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: Contact[]) => void;
                share: (
                    payload: Partial<Contact> & { name?: string; phone: string },
                ) => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<Contact[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: Contact) => Promise<void>;
            };
            favoriteContacts: Readable<Contact[]>;
            shareContact: (
                firstname: string,
                phone: string,
                lastname?: string,
            ) => Promise<void>;
        }
      • OS Service Hook for accessing address book contacts and sharing contacts.

        Returns {
            addContact: (
                firstname: string,
                phone: string,
                lastname?: string,
                avatar?: string,
                favorite?: boolean,
            ) => Promise<Contact>;
            contactsStore: {
                add: (draft: TDraft) => Promise<Contact>;
                delete: (id: number) => Promise<void>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: Contact[]) => void;
                share: (
                    payload: Partial<Contact> & { name?: string; phone: string },
                ) => Promise<void>;
                subscribe: (
                    this: void,
                    run: Subscriber<Contact[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: Contact) => Promise<void>;
            };
            favoriteContacts: Readable<Contact[]>;
            shareContact: (
                firstname: string,
                phone: string,
                lastname?: string,
            ) => Promise<void>;
        }

    deepLink: (appId: string, handle: () => boolean) => void

    Type Declaration

      • (appId: string, handle: () => boolean): void
      • Implementation of the useDeepLink facet — see the useDeepLink hook doc for the usage contract (the residency rules on when handle runs and what its return means).

        A rune file rather than a plain module: the handler reads props and stores, and only an effect re-runs when those change.

        Parameters

        • appId: string
        • handle: () => boolean

        Returns void

    devTools: () => {
        devToolsUnlocked: Writable<boolean>;
        lock: () => void;
        unlock: () => void;
    }

    Type Declaration

      • (): {
            devToolsUnlocked: Writable<boolean>;
            lock: () => void;
            unlock: () => void;
        }
      • OS Service Hook for the Developer Tools reveal.

        Settings was importing ../../store/devtools directly, which §2.7 prohibits outright — reaching into a shell store is the exact thing that stops an app working as a standalone add-on. It is the only store an app module was still reaching for.

        The flag is session-scoped and not persisted; see store/devtools.ts for why.

        Returns { devToolsUnlocked: Writable<boolean>; lock: () => void; unlock: () => void }

        • devToolsUnlocked: Writable<boolean>

          Whether the Developer Tools group is currently revealed.

        • lock: () => void
        • unlock: () => void
    display: () => {
        displaySize: Writable<number>;
        displaySizeDefault: number;
        homeGridColumns: Writable<number>;
        homeGridColumnsDefault: number;
        homeGridColumnsMax: number;
        homeGridColumnsMin: number;
        homeGridRows: Writable<number>;
        homeGridRowsDefault: number;
        homeGridRowsMax: number;
        homeGridRowsMin: number;
        isSizeLimited: Readable<boolean>;
        phoneBox: Readable<{ height: number; width: number }>;
        phoneScale: Readable<number>;
        setDisplaySize: (size: number) => void;
        setHomeGridSize: (columns: number, rows: number) => void;
    }

    Type Declaration

      • (): {
            displaySize: Writable<number>;
            displaySizeDefault: number;
            homeGridColumns: Writable<number>;
            homeGridColumnsDefault: number;
            homeGridColumnsMax: number;
            homeGridColumnsMin: number;
            homeGridRows: Writable<number>;
            homeGridRowsDefault: number;
            homeGridRowsMax: number;
            homeGridRowsMin: number;
            isSizeLimited: Readable<boolean>;
            phoneBox: Readable<{ height: number; width: number }>;
            phoneScale: Readable<number>;
            setDisplaySize: (size: number) => void;
            setHomeGridSize: (columns: number, rows: number) => void;
        }
      • How big the phone is drawn on screen.

        Its own hook rather than a corner of useSystemHardware, for the reason useClock was split out: that hook means battery, signal and the volume buttons, and how large the frame is rendered is none of those. It is the window's business, and the only app with a reason to touch it is Settings.

        Returns {
            displaySize: Writable<number>;
            displaySizeDefault: number;
            homeGridColumns: Writable<number>;
            homeGridColumnsDefault: number;
            homeGridColumnsMax: number;
            homeGridColumnsMin: number;
            homeGridRows: Writable<number>;
            homeGridRowsDefault: number;
            homeGridRowsMax: number;
            homeGridRowsMin: number;
            isSizeLimited: Readable<boolean>;
            phoneBox: Readable<{ height: number; width: number }>;
            phoneScale: Readable<number>;
            setDisplaySize: (size: number) => void;
            setHomeGridSize: (columns: number, rows: number) => void;
        }

        • displaySize: Writable<number>

          The Display > Phone Size setting, 0-100. Writable: Settings moves it.

        • displaySizeDefault: number

          Where the slider starts, so a Reset control needs no second copy of the number.

        • homeGridColumns: Writable<number>

          Home Screen Grid — columns/rows, and their adjustable bounds.

        • homeGridColumnsDefault: number
        • homeGridColumnsMax: number
        • homeGridColumnsMin: number
        • homeGridRows: Writable<number>
        • homeGridRowsDefault: number
        • homeGridRowsMax: number
        • homeGridRowsMin: number
        • isSizeLimited: Readable<boolean>

          True when the window is smaller than the setting asks for, and is winning.

        • phoneBox: Readable<{ height: number; width: number }>

          The rendered size in CSS pixels, for showing the player what they picked.

        • phoneScale: Readable<number>

          The zoom actually applied, after fitting to the window. Read-only.

        • setDisplaySize: (size: number) => void
        • setHomeGridSize: (columns: number, rows: number) => void

          Applies a new grid size and reflows anything the shrink pushed out of bounds. The setter alone would leave those items structurally valid but unreachable — a shrink is the one time homeGridItems needs touching from outside homeGrid.ts itself, so this bundles the write and the reflow into one call rather than asking every caller to remember the second step.

    highscores: () => {
        getLeaderboard: (app: string) => Promise<LeaderboardEntry[]>;
        submitScore: (app: string, score: number) => Promise<void>;
    }

    Type Declaration

      • (): {
            getLeaderboard: (app: string) => Promise<LeaderboardEntry[]>;
            submitScore: (app: string, score: number) => Promise<void>;
        }
      • OS Service Hook for the shared, cross-game leaderboard table.

        The only door into gphone_highscores — no app, core or add-on, reaches the table any other way. app is the game's own id ('snek' today); a second game reuses this hook rather than adding a table.

        Returns {
            getLeaderboard: (app: string) => Promise<LeaderboardEntry[]>;
            submitScore: (app: string, score: number) => Promise<void>;
        }

        • getLeaderboard: (app: string) => Promise<LeaderboardEntry[]>
        • submitScore: (app: string, score: number) => Promise<void>

          Fire-and-forget: a failed submit must never block the game-over screen.

    keybinds: () => {
        bindings: Readable<Record<string, string>>;
        findConflict: (actionId: string, key: string) => KeybindAction | undefined;
        groups: Readable<KeybindGroup[]>;
        onKeybind: (
            actionId: string,
            handler: () => void,
            appId?: string,
        ) => () => void;
        resetBindings: () => void;
        setBinding: (actionId: string, key: string) => void;
    }

    Type Declaration

      • (): {
            bindings: Readable<Record<string, string>>;
            findConflict: (actionId: string, key: string) => KeybindAction | undefined;
            groups: Readable<KeybindGroup[]>;
            onKeybind: (
                actionId: string,
                handler: () => void,
                appId?: string,
            ) => () => void;
            resetBindings: () => void;
            setBinding: (actionId: string, key: string) => void;
        }
      • Implementation of the useKeybinds facet — see the useKeybinds hook doc for the usage contract.

        Returns {
            bindings: Readable<Record<string, string>>;
            findConflict: (actionId: string, key: string) => KeybindAction | undefined;
            groups: Readable<KeybindGroup[]>;
            onKeybind: (
                actionId: string,
                handler: () => void,
                appId?: string,
            ) => () => void;
            resetBindings: () => void;
            setBinding: (actionId: string, key: string) => void;
        }

        • bindings: Readable<Record<string, string>>

          Live map of actionId -> bound key.

        • findConflict: (actionId: string, key: string) => KeybindAction | undefined

          The action already using this key in the same context, if any. Two actions may share a key when their contexts are disjoint — Enter is both Answer Call and the camera shutter, and only one is ever eligible.

        • groups: Readable<KeybindGroup[]>

          Everything configurable from gPhone's own Shortcuts screen, grouped by owner.

          Core first (ownerId: 'core'), then one group per installed app that declares its own keybinds, sorted alphabetically by ownerLabel. An app with no declared keybinds contributes no group at all, rather than an empty one.

        • onKeybind: (actionId: string, handler: () => void, appId?: string) => () => void

          Claim an action for as long as this component is mounted.

          Pass appId for anything an app claims. Apps are resident, so the claim outlives the app being on screen, and without an owner the dispatcher hands the action to whichever app registered last — see the registry note in shell/state/keybinds.ts. Only actions carrying their own when: 'app:…' context are safe without it, and naming the app costs nothing either way.

        • resetBindings: () => void
        • setBinding: (actionId: string, key: string) => void
    lifecycle: (
        appId: string,
    ) => {
        consumeDeepLink: () => void;
        currentApp: Writable<RunningApp>;
        goHome: () => void;
        onBack: (handler: () => void) => () => void;
    }

    Type Declaration

      • (
            appId: string,
        ): {
            consumeDeepLink: () => void;
            currentApp: Writable<RunningApp>;
            goHome: () => void;
            onBack: (handler: () => void) => () => void;
        }
      • GPHONE-27: the one implicit, no-permission door a sandboxed add-on's baseline plumbing reaches the shell through — onAppForeground/onAppUnmount (this file's own twin, over currentApp), useDeepLink (consumeDeepLink), the onback prop every app gets (goHome), and useAppLevels's physical Back binding (onBack).

        Replaces IframeHostServer.ts's old isImplicitNavPlumbing — a hand-maintained facet/member allow-list keyed on literal strings, which is exactly the shape of bug GPHONE-31 was: a legitimate implicit caller (consumeDeepLink) simply wasn't on the list. A dedicated facet with its own null permission needs no such list — every member here is implicit by construction, and the general navigation/keybinds facets this used to borrow members from now require their real declared permissions unconditionally, with no exemption at all.

        appId is this facet's one factory argument, so IframeHostServer.ts pins it via APP_SCOPED_FACETS exactly like storage/deepLink — a raw message naming lifecycle directly can state its own facet id (factoryArgs[0]) all it wants; the shell replaces it before the factory ever sees it.

        Parameters

        • appId: string

        Returns {
            consumeDeepLink: () => void;
            currentApp: Writable<RunningApp>;
            goHome: () => void;
            onBack: (handler: () => void) => () => void;
        }

        • consumeDeepLink: () => void

          Mark this app's deep-link props as handled, so they do not fire again. See useDeepLink's doc for the usage contract.

        • currentApp: Writable<RunningApp>

          Whatever is on screen. home is the shell, not an app, and is never resident.

        • goHome: () => void
        • onBack: (handler: () => void) => () => void

          Claim the physical Back key for as long as this component is mounted.

    location: () => {
        setWaypoint: (x: number, y: number) => Promise<void>;
        shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
    }

    Type Declaration

      • (): {
            setWaypoint: (x: number, y: number) => Promise<void>;
            shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
        }
      • Share the caller's current in-game position, and set a GPS waypoint from a location a message already carries. An action pair rather than a data subscription — closer in shape to useCamera than to useContacts — so there is no store here to read.

        Returns {
            setWaypoint: (x: number, y: number) => Promise<void>;
            shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
        }

    mail: () => {
        addReceivedMail: (newMail: Mail) => void;
        archiveMail: (id: number, archiveState?: boolean) => Promise<void>;
        deleteMail: (id: number) => Promise<void>;
        mailStore: {
            add: (draft: TDraft) => Promise<Mail>;
            addReceivedMail: (incoming: Mail) => void;
            archive: (id: number, archive?: boolean) => Promise<void>;
            delete: (id: number) => Promise<void>;
            load: () => Promise<void>;
            loaded: {
                subscribe: (
                    this: void,
                    run: Subscriber<boolean>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            markAsRead: (id: number) => Promise<void>;
            patch: (id: number, changes: Partial<T>) => void;
            set: (rows: Mail[]) => void;
            subscribe: (
                this: void,
                run: Subscriber<Mail[]>,
                invalidate?: () => void,
            ) => Unsubscriber;
            update: (row: Mail) => Promise<void>;
        };
        markAsRead: (id: number) => Promise<void>;
        unreadMailCount: Readable<number>;
    }

    Type Declaration

      • (): {
            addReceivedMail: (newMail: Mail) => void;
            archiveMail: (id: number, archiveState?: boolean) => Promise<void>;
            deleteMail: (id: number) => Promise<void>;
            mailStore: {
                add: (draft: TDraft) => Promise<Mail>;
                addReceivedMail: (incoming: Mail) => void;
                archive: (id: number, archive?: boolean) => Promise<void>;
                delete: (id: number) => Promise<void>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                markAsRead: (id: number) => Promise<void>;
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: Mail[]) => void;
                subscribe: (
                    this: void,
                    run: Subscriber<Mail[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: Mail) => Promise<void>;
            };
            markAsRead: (id: number) => Promise<void>;
            unreadMailCount: Readable<number>;
        }
      • OS Service Hook for email messaging.

        Returns {
            addReceivedMail: (newMail: Mail) => void;
            archiveMail: (id: number, archiveState?: boolean) => Promise<void>;
            deleteMail: (id: number) => Promise<void>;
            mailStore: {
                add: (draft: TDraft) => Promise<Mail>;
                addReceivedMail: (incoming: Mail) => void;
                archive: (id: number, archive?: boolean) => Promise<void>;
                delete: (id: number) => Promise<void>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                markAsRead: (id: number) => Promise<void>;
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: Mail[]) => void;
                subscribe: (
                    this: void,
                    run: Subscriber<Mail[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: Mail) => Promise<void>;
            };
            markAsRead: (id: number) => Promise<void>;
            unreadMailCount: Readable<number>;
        }

    marketplace: () => {
        feedStore: Writable<ListingPage>;
        loadFeed: () => Promise<void>;
        loadMine: () => Promise<void>;
        markSold: (id: number) => Promise<boolean>;
        mineStore: Writable<ListingPage>;
        postListing: (input: CreateListingInput) => Promise<Listing>;
        removeListing: (id: number) => Promise<boolean>;
        searchListings: (q: string) => Promise<ListingPage>;
        viewListing: (
            id: number,
        ) => Promise<
            Listing & { contactPhone: string | null; isOwn: boolean }
            | null,
        >;
    }

    Type Declaration

      • (): {
            feedStore: Writable<ListingPage>;
            loadFeed: () => Promise<void>;
            loadMine: () => Promise<void>;
            markSold: (id: number) => Promise<boolean>;
            mineStore: Writable<ListingPage>;
            postListing: (input: CreateListingInput) => Promise<Listing>;
            removeListing: (id: number) => Promise<boolean>;
            searchListings: (q: string) => Promise<ListingPage>;
            viewListing: (
                id: number,
            ) => Promise<
                Listing & { contactPhone: string | null; isOwn: boolean }
                | null,
            >;
        }
      • OS Service Hook for Marketplace.

        Returns {
            feedStore: Writable<ListingPage>;
            loadFeed: () => Promise<void>;
            loadMine: () => Promise<void>;
            markSold: (id: number) => Promise<boolean>;
            mineStore: Writable<ListingPage>;
            postListing: (input: CreateListingInput) => Promise<Listing>;
            removeListing: (id: number) => Promise<boolean>;
            searchListings: (q: string) => Promise<ListingPage>;
            viewListing: (
                id: number,
            ) => Promise<
                Listing & { contactPhone: string | null; isOwn: boolean }
                | null,
            >;
        }

        • feedStore: Writable<ListingPage>

          The public feed — active listings, newest first.

        • loadFeed: () => Promise<void>
        • loadMine: () => Promise<void>
        • markSold: (id: number) => Promise<boolean>
        • mineStore: Writable<ListingPage>

          The caller's own listings, every status.

        • postListing: (input: CreateListingInput) => Promise<Listing>
        • removeListing: (id: number) => Promise<boolean>
        • searchListings: (q: string) => Promise<ListingPage>
        • viewListing: (
              id: number,
          ) => Promise<
              Listing & { contactPhone: string | null; isOwn: boolean }
              | null,
          >
    media: () => {
        capturePhoto: (data: string) => Promise<MediaItem>;
        deletePhoto: (id: number) => Promise<void>;
        dropNearby: (mediaId: number) => Promise<{ count: number }>;
        media: {
            add: (draft: TDraft) => Promise<MediaItem>;
            delete: (id: number) => Promise<void>;
            dropNearby: (mediaId: number) => Promise<{ count: number }>;
            load: () => Promise<void>;
            loaded: {
                subscribe: (
                    this: void,
                    run: Subscriber<boolean>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            patch: (id: number, changes: Partial<T>) => void;
            set: (rows: MediaItem[]) => void;
            setWaypoint: (x: number, y: number) => Promise<void>;
            shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
            subscribe: (
                this: void,
                run: Subscriber<MediaItem[]>,
                invalidate?: () => void,
            ) => Unsubscriber;
            update: (row: MediaItem) => Promise<void>;
        };
    }

    Type Declaration

      • (): {
            capturePhoto: (data: string) => Promise<MediaItem>;
            deletePhoto: (id: number) => Promise<void>;
            dropNearby: (mediaId: number) => Promise<{ count: number }>;
            media: {
                add: (draft: TDraft) => Promise<MediaItem>;
                delete: (id: number) => Promise<void>;
                dropNearby: (mediaId: number) => Promise<{ count: number }>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: MediaItem[]) => void;
                setWaypoint: (x: number, y: number) => Promise<void>;
                shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
                subscribe: (
                    this: void,
                    run: Subscriber<MediaItem[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: MediaItem) => Promise<void>;
            };
        }
      • The media gallery — the client face of the media service.

        Split out of useCamera, which owned both the hardware and the library. They are used by different apps for different reasons: Media reads the gallery and never touches the shutter, and an app attaching an image to a message wants neither.

        Returns {
            capturePhoto: (data: string) => Promise<MediaItem>;
            deletePhoto: (id: number) => Promise<void>;
            dropNearby: (mediaId: number) => Promise<{ count: number }>;
            media: {
                add: (draft: TDraft) => Promise<MediaItem>;
                delete: (id: number) => Promise<void>;
                dropNearby: (mediaId: number) => Promise<{ count: number }>;
                load: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                patch: (id: number, changes: Partial<T>) => void;
                set: (rows: MediaItem[]) => void;
                setWaypoint: (x: number, y: number) => Promise<void>;
                shareLocation: () => Promise<{ id: number; media: MediaPreview }>;
                subscribe: (
                    this: void,
                    run: Subscriber<MediaItem[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
                update: (row: MediaItem) => Promise<void>;
            };
        }

    messages: () => {
        addReceivedMessage: (
            message: {
                avatar?: string;
                conversation_id?: number;
                created_at?: string;
                message?: string;
                phone?: string;
                reply_to_id?: number | null;
                senderName?: string;
            },
        ) => void;
        conversationsStore: {
            activeConversationId: {
                subscribe: (
                    this: void,
                    run: Subscriber<number | null>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            addReceivedMessage: (
                incoming: {
                    avatar?: string;
                    conversation_id?: number;
                    created_at?: string;
                    message?: string;
                    phone?: string;
                    reply_to_id?: number | null;
                    senderName?: string;
                },
            ) => void;
            archiveConversation: (
                conversationId: number,
                archive?: boolean,
            ) => Promise<void>;
            deleteConversation: (conversationId: number) => Promise<void>;
            loadConversations: () => Promise<void>;
            loaded: {
                subscribe: (
                    this: void,
                    run: Subscriber<boolean>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            loadMessages: (conversationId: number) => Promise<void>;
            markAsRead: (conversationId: number) => Promise<void>;
            messages: {
                subscribe: (
                    this: void,
                    run: Subscriber<Record<number, UIMessage[]>>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            renameConversation: (
                conversationId: number,
                name: string,
            ) => Promise<void>;
            sendMessage: (
                conversationId: number,
                message: string,
                attachments?: { attachment?: string; photo_id: number }[],
                replyToId?: number | null,
            ) => Promise<Message | null>;
            setActiveConversationId: (id: number | null) => void;
            startConversation: (
                phone: string,
                isGroup?: boolean,
            ) => Promise<UIConversation | null>;
            subscribe: (
                this: void,
                run: Subscriber<UIConversation[]>,
                invalidate?: () => void,
            ) => Unsubscriber;
        };
        sendMessage: (
            conversationId: number,
            text: string,
        ) => Promise<Message | null>;
        startText: (phone: string) => void;
        unreadMessagesCount: Readable<number>;
    }

    Type Declaration

      • (): {
            addReceivedMessage: (
                message: {
                    avatar?: string;
                    conversation_id?: number;
                    created_at?: string;
                    message?: string;
                    phone?: string;
                    reply_to_id?: number | null;
                    senderName?: string;
                },
            ) => void;
            conversationsStore: {
                activeConversationId: {
                    subscribe: (
                        this: void,
                        run: Subscriber<number | null>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                addReceivedMessage: (
                    incoming: {
                        avatar?: string;
                        conversation_id?: number;
                        created_at?: string;
                        message?: string;
                        phone?: string;
                        reply_to_id?: number | null;
                        senderName?: string;
                    },
                ) => void;
                archiveConversation: (
                    conversationId: number,
                    archive?: boolean,
                ) => Promise<void>;
                deleteConversation: (conversationId: number) => Promise<void>;
                loadConversations: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                loadMessages: (conversationId: number) => Promise<void>;
                markAsRead: (conversationId: number) => Promise<void>;
                messages: {
                    subscribe: (
                        this: void,
                        run: Subscriber<Record<number, UIMessage[]>>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                renameConversation: (
                    conversationId: number,
                    name: string,
                ) => Promise<void>;
                sendMessage: (
                    conversationId: number,
                    message: string,
                    attachments?: { attachment?: string; photo_id: number }[],
                    replyToId?: number | null,
                ) => Promise<Message | null>;
                setActiveConversationId: (id: number | null) => void;
                startConversation: (
                    phone: string,
                    isGroup?: boolean,
                ) => Promise<UIConversation | null>;
                subscribe: (
                    this: void,
                    run: Subscriber<UIConversation[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            sendMessage: (
                conversationId: number,
                text: string,
            ) => Promise<Message | null>;
            startText: (phone: string) => void;
            unreadMessagesCount: Readable<number>;
        }
      • OS Service Hook for accessing SMS messaging.

        Returns {
            addReceivedMessage: (
                message: {
                    avatar?: string;
                    conversation_id?: number;
                    created_at?: string;
                    message?: string;
                    phone?: string;
                    reply_to_id?: number | null;
                    senderName?: string;
                },
            ) => void;
            conversationsStore: {
                activeConversationId: {
                    subscribe: (
                        this: void,
                        run: Subscriber<number | null>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                addReceivedMessage: (
                    incoming: {
                        avatar?: string;
                        conversation_id?: number;
                        created_at?: string;
                        message?: string;
                        phone?: string;
                        reply_to_id?: number | null;
                        senderName?: string;
                    },
                ) => void;
                archiveConversation: (
                    conversationId: number,
                    archive?: boolean,
                ) => Promise<void>;
                deleteConversation: (conversationId: number) => Promise<void>;
                loadConversations: () => Promise<void>;
                loaded: {
                    subscribe: (
                        this: void,
                        run: Subscriber<boolean>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                loadMessages: (conversationId: number) => Promise<void>;
                markAsRead: (conversationId: number) => Promise<void>;
                messages: {
                    subscribe: (
                        this: void,
                        run: Subscriber<Record<number, UIMessage[]>>,
                        invalidate?: () => void,
                    ) => Unsubscriber;
                };
                renameConversation: (
                    conversationId: number,
                    name: string,
                ) => Promise<void>;
                sendMessage: (
                    conversationId: number,
                    message: string,
                    attachments?: { attachment?: string; photo_id: number }[],
                    replyToId?: number | null,
                ) => Promise<Message | null>;
                setActiveConversationId: (id: number | null) => void;
                startConversation: (
                    phone: string,
                    isGroup?: boolean,
                ) => Promise<UIConversation | null>;
                subscribe: (
                    this: void,
                    run: Subscriber<UIConversation[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
            sendMessage: (
                conversationId: number,
                text: string,
            ) => Promise<Message | null>;
            startText: (phone: string) => void;
            unreadMessagesCount: Readable<number>;
        }

        • addReceivedMessage: (
              message: {
                  avatar?: string;
                  conversation_id?: number;
                  created_at?: string;
                  message?: string;
                  phone?: string;
                  reply_to_id?: number | null;
                  senderName?: string;
              },
          ) => void
        • conversationsStore: {
              activeConversationId: {
                  subscribe: (
                      this: void,
                      run: Subscriber<number | null>,
                      invalidate?: () => void,
                  ) => Unsubscriber;
              };
              addReceivedMessage: (
                  incoming: {
                      avatar?: string;
                      conversation_id?: number;
                      created_at?: string;
                      message?: string;
                      phone?: string;
                      reply_to_id?: number | null;
                      senderName?: string;
                  },
              ) => void;
              archiveConversation: (
                  conversationId: number,
                  archive?: boolean,
              ) => Promise<void>;
              deleteConversation: (conversationId: number) => Promise<void>;
              loadConversations: () => Promise<void>;
              loaded: {
                  subscribe: (
                      this: void,
                      run: Subscriber<boolean>,
                      invalidate?: () => void,
                  ) => Unsubscriber;
              };
              loadMessages: (conversationId: number) => Promise<void>;
              markAsRead: (conversationId: number) => Promise<void>;
              messages: {
                  subscribe: (
                      this: void,
                      run: Subscriber<Record<number, UIMessage[]>>,
                      invalidate?: () => void,
                  ) => Unsubscriber;
              };
              renameConversation: (conversationId: number, name: string) => Promise<void>;
              sendMessage: (
                  conversationId: number,
                  message: string,
                  attachments?: { attachment?: string; photo_id: number }[],
                  replyToId?: number | null,
              ) => Promise<Message | null>;
              setActiveConversationId: (id: number | null) => void;
              startConversation: (
                  phone: string,
                  isGroup?: boolean,
              ) => Promise<UIConversation | null>;
              subscribe: (
                  this: void,
                  run: Subscriber<UIConversation[]>,
                  invalidate?: () => void,
              ) => Unsubscriber;
          }
          • activeConversationId: {
                subscribe: (
                    this: void,
                    run: Subscriber<number | null>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            }
            • subscribe: (
                  this: void,
                  run: Subscriber<number | null>,
                  invalidate?: () => void,
              ) => Unsubscriber
          • addReceivedMessage: (
                incoming: {
                    avatar?: string;
                    conversation_id?: number;
                    created_at?: string;
                    message?: string;
                    phone?: string;
                    reply_to_id?: number | null;
                    senderName?: string;
                },
            ) => void
          • archiveConversation: (conversationId: number, archive?: boolean) => Promise<void>
          • deleteConversation: (conversationId: number) => Promise<void>
          • loadConversations: () => Promise<void>
          • loaded: {
                subscribe: (
                    this: void,
                    run: Subscriber<boolean>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            }
            • subscribe: (this: void, run: Subscriber<boolean>, invalidate?: () => void) => Unsubscriber
          • loadMessages: (conversationId: number) => Promise<void>
          • markAsRead: (conversationId: number) => Promise<void>
          • messages: {
                subscribe: (
                    this: void,
                    run: Subscriber<Record<number, UIMessage[]>>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            }
            • subscribe: (
                  this: void,
                  run: Subscriber<Record<number, UIMessage[]>>,
                  invalidate?: () => void,
              ) => Unsubscriber
          • renameConversation: (conversationId: number, name: string) => Promise<void>
          • sendMessage: (
                conversationId: number,
                message: string,
                attachments?: { attachment?: string; photo_id: number }[],
                replyToId?: number | null,
            ) => Promise<Message | null>
          • setActiveConversationId: (id: number | null) => void
          • startConversation: (phone: string, isGroup?: boolean) => Promise<UIConversation | null>
          • subscribe: (
                this: void,
                run: Subscriber<UIConversation[]>,
                invalidate?: () => void,
            ) => Unsubscriber
        • sendMessage: (conversationId: number, text: string) => Promise<Message | null>
        • startText: (phone: string) => void

          Open Messages and start (or resume) a conversation with a bare phone number — no saved Contact required. See GPHONE-15.

        • unreadMessagesCount: Readable<number>
    navigation: () => {
        closePhone: () => void;
        currentApp: Writable<RunningApp>;
        goHome: () => void;
        openApp: (appName: string, props?: Record<string, unknown>) => void;
    }

    Type Declaration

      • (): {
            closePhone: () => void;
            currentApp: Writable<RunningApp>;
            goHome: () => void;
            openApp: (appName: string, props?: Record<string, unknown>) => void;
        }
      • OS Service Hook for phone navigation (opening apps, returning home, closing phone shell).

        consumeDeepLink used to live here, reachable only through an isImplicitNavPlumbing exemption since useDeepLink is implicit and no real caller ever held navigation permission for it. Moved to the dedicated lifecycle facet (GPHONE-27) — this facet's members now all genuinely require the navigation permission, no exemption at all.

        Returns {
            closePhone: () => void;
            currentApp: Writable<RunningApp>;
            goHome: () => void;
            openApp: (appName: string, props?: Record<string, unknown>) => void;
        }

        • closePhone: () => void
        • currentApp: Writable<RunningApp>

          Whatever is on screen. home is the shell, not an app, and is never resident.

        • goHome: () => void
        • openApp: (appName: string, props?: Record<string, unknown>) => void
    notifications: (
        appId?: string,
    ) => {
        clear: (ids: number[]) => Promise<void>;
        clearAll: (targetAppId?: string) => Promise<void>;
        load: () => Promise<void>;
        loaded: Writable<boolean>;
        markRead: (ids: number[]) => Promise<void>;
        notificationsStore: Readable<NotificationItem[]>;
        totalUnread: Readable<number>;
        unreadCount: Readable<number>;
    }

    Type Declaration

      • (
            appId?: string,
        ): {
            clear: (ids: number[]) => Promise<void>;
            clearAll: (targetAppId?: string) => Promise<void>;
            load: () => Promise<void>;
            loaded: Writable<boolean>;
            markRead: (ids: number[]) => Promise<void>;
            notificationsStore: Readable<NotificationItem[]>;
            totalUnread: Readable<number>;
            unreadCount: Readable<number>;
        }
      • SDK Hook providing OS persistent notifications, unread counts, and management actions.

        Parameters

        • OptionalappId: string

        Returns {
            clear: (ids: number[]) => Promise<void>;
            clearAll: (targetAppId?: string) => Promise<void>;
            load: () => Promise<void>;
            loaded: Writable<boolean>;
            markRead: (ids: number[]) => Promise<void>;
            notificationsStore: Readable<NotificationItem[]>;
            totalUnread: Readable<number>;
            unreadCount: Readable<number>;
        }

        • clear: (ids: number[]) => Promise<void>
        • clearAll: (targetAppId?: string) => Promise<void>
        • load: () => Promise<void>
        • loaded: Writable<boolean>

          Shared across every caller: the shade is one list, so the first fetch is one fetch.

        • markRead: (ids: number[]) => Promise<void>
        • notificationsStore: Readable<NotificationItem[]>
        • totalUnread: Readable<number>
        • unreadCount: Readable<number>
    notificationSettings: () => {
        badgesEnabled: Writable<boolean>;
        notificationSoundEnabled: Writable<boolean>;
        toastsEnabled: Writable<boolean>;
    }

    Type Declaration

      • (): {
            badgesEnabled: Writable<boolean>;
            notificationSoundEnabled: Writable<boolean>;
            toastsEnabled: Writable<boolean>;
        }
      • OS Service Hook for Notification user preferences.

        Returns {
            badgesEnabled: Writable<boolean>;
            notificationSoundEnabled: Writable<boolean>;
            toastsEnabled: Writable<boolean>;
        }

        • badgesEnabled: Writable<boolean>
        • notificationSoundEnabled: Writable<boolean>
        • toastsEnabled: Writable<boolean>

          Persistent user settings for OS notifications.

    onAppForeground: (appId: string, handler: () => void) => () => void

    Type Declaration

      • (appId: string, handler: () => void): () => void
      • Implementation of the onAppForeground facet — see the onAppForeground hook doc for the usage contract (the stale-balance rationale for why this exists).

        Parameters

        • appId: string
        • handler: () => void

        Returns () => void

    onAppUnmount: (handler: () => void) => void

    Type Declaration

      • (handler: () => void): void
      • Executes a cleanup callback when the application component is unmounted or closed.

        Parameters

        • handler: () => void

        Returns void

    persisted: <T>(
        appId: string,
        key: string,
        initial: T,
        options?: PersistedOptions<T>,
    ) => Writable<T>

    Type Declaration

      • <T>(
            appId: string,
            key: string,
            initial: T,
            options?: PersistedOptions<T>,
        ): Writable<T>
      • Implementation of the usePersisted facet — see the usePersisted hook doc for the usage contract.

        Type Parameters

        • T

        Parameters

        Returns Writable<T>

    phoneNotification: () => {
        dismissNotification: (id: string) => void;
        sendNotification: (options: SendNotificationOptions) => string;
        toast: {
            archive: (id: string) => Promise<void>;
            clear: () => void;
            dismiss: (id: string) => void;
            pauseDismiss: (id: string) => void;
            resumeDismiss: (id: string, delay?: number) => void;
            show: (options: Partial<ToastMessage> & { message: string }) => string;
            showCall: (
                options: {
                    name?: string;
                    number: string;
                    onAccept: () => void | Promise<void>;
                    onDecline?: () => void | Promise<void>;
                    onExpire?: () => void | Promise<void>;
                },
            ) => string;
            showContactShare: (
                options: {
                    avatar?: string;
                    name: string;
                    onAccept: () => void | Promise<void>;
                    onClick?: () => void | Promise<void>;
                    onDecline?: () => void | Promise<void>;
                    phone: string;
                },
            ) => string;
            showIncomingMessage: (
                options: {
                    avatar?: string;
                    message: string;
                    onClick?: () => void;
                    onReply: (replyText: string) => void | Promise<void>;
                    sender: string;
                },
            ) => string;
            showMail: (
                options: { onClick?: () => void; sender: string; subject: string },
            ) => string;
            subscribe: (
                this: void,
                run: Subscriber<ToastMessage[]>,
                invalidate?: () => void,
            ) => Unsubscriber;
        };
    }

    Type Declaration

      • (): {
            dismissNotification: (id: string) => void;
            sendNotification: (options: SendNotificationOptions) => string;
            toast: {
                archive: (id: string) => Promise<void>;
                clear: () => void;
                dismiss: (id: string) => void;
                pauseDismiss: (id: string) => void;
                resumeDismiss: (id: string, delay?: number) => void;
                show: (options: Partial<ToastMessage> & { message: string }) => string;
                showCall: (
                    options: {
                        name?: string;
                        number: string;
                        onAccept: () => void | Promise<void>;
                        onDecline?: () => void | Promise<void>;
                        onExpire?: () => void | Promise<void>;
                    },
                ) => string;
                showContactShare: (
                    options: {
                        avatar?: string;
                        name: string;
                        onAccept: () => void | Promise<void>;
                        onClick?: () => void | Promise<void>;
                        onDecline?: () => void | Promise<void>;
                        phone: string;
                    },
                ) => string;
                showIncomingMessage: (
                    options: {
                        avatar?: string;
                        message: string;
                        onClick?: () => void;
                        onReply: (replyText: string) => void | Promise<void>;
                        sender: string;
                    },
                ) => string;
                showMail: (
                    options: { onClick?: () => void; sender: string; subject: string },
                ) => string;
                subscribe: (
                    this: void,
                    run: Subscriber<ToastMessage[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
        }
      • OS Service Hook for sending toast notifications and system alerts.

        Returns {
            dismissNotification: (id: string) => void;
            sendNotification: (options: SendNotificationOptions) => string;
            toast: {
                archive: (id: string) => Promise<void>;
                clear: () => void;
                dismiss: (id: string) => void;
                pauseDismiss: (id: string) => void;
                resumeDismiss: (id: string, delay?: number) => void;
                show: (options: Partial<ToastMessage> & { message: string }) => string;
                showCall: (
                    options: {
                        name?: string;
                        number: string;
                        onAccept: () => void | Promise<void>;
                        onDecline?: () => void | Promise<void>;
                        onExpire?: () => void | Promise<void>;
                    },
                ) => string;
                showContactShare: (
                    options: {
                        avatar?: string;
                        name: string;
                        onAccept: () => void | Promise<void>;
                        onClick?: () => void | Promise<void>;
                        onDecline?: () => void | Promise<void>;
                        phone: string;
                    },
                ) => string;
                showIncomingMessage: (
                    options: {
                        avatar?: string;
                        message: string;
                        onClick?: () => void;
                        onReply: (replyText: string) => void | Promise<void>;
                        sender: string;
                    },
                ) => string;
                showMail: (
                    options: { onClick?: () => void; sender: string; subject: string },
                ) => string;
                subscribe: (
                    this: void,
                    run: Subscriber<ToastMessage[]>,
                    invalidate?: () => void,
                ) => Unsubscriber;
            };
        }

    report: () => { submit: (input: SubmitReportInput) => Promise<void> }

    Type Declaration

      • (): { submit: (input: SubmitReportInput) => Promise<void> }
      • Report a piece of content.

        Split from useReports — the moderation queue — because the two are different capabilities: every social app lets a player file a report, and only Admin reads them. ReportDialog is the usual caller; an app that files reports from its own UI uses this.

        Returns { submit: (input: SubmitReportInput) => Promise<void> }

    reports: () => {
        loadPendingReports: () => Promise<void>;
        loadReportHistory: () => Promise<void>;
        pendingReportCount: Readable<number>;
        pendingReports: Writable<Report[]>;
        reopenReport: (id: number) => Promise<void>;
        resolvedReports: Writable<Report[]>;
        resolveReport: (
            id: number,
            action: "moderate" | "dismiss",
        ) => Promise<void>;
    }
    service: (
        serviceId: string,
    ) => {
        call: <T = unknown>(
            action: string,
            data?: unknown,
            defaultValue?: T,
        ) => Promise<T>;
        id: string;
    }

    Type Declaration

      • (
            serviceId: string,
        ): {
            call: <T = unknown>(
                action: string,
                data?: unknown,
                defaultValue?: T,
            ) => Promise<T>;
            id: string;
        }
      • Implementation of the useService facet — see the useService hook doc for the usage contract (what this does and does not change, whose service an id may name).

        Parameters

        • serviceId: string

        Returns {
            call: <T = unknown>(
                action: string,
                data?: unknown,
                defaultValue?: T,
            ) => Promise<T>;
            id: string;
        }

        • call: <T = unknown>(action: string, data?: unknown, defaultValue?: T) => Promise<T>

          Call one action and wait for the reply.

          defaultValue behaves as it does everywhere else in the SDK: a failed round trip resolves to it rather than throwing, so a missing server half degrades to an empty list instead of a crashed app. Omit it when a failure should surface — a write wrapped in useAppAction wants the error so it can toast it.

        • id: string
    sound: () => { play: (effect: SoundEffect) => void }

    Type Declaration

      • (): { play: (effect: SoundEffect) => void }
      • The phone's sound effects.

        SoundService has been complete and unit-tested since the shell existed — volume- and mute-aware, used by AppIcon, ToggleSwitch and SegmentedControl. It was simply never exported, so the only route to it was ../../shell/state/audio, which boundary.test.ts rejects. An app had no sanctioned way to make a noise.

        That is the SegmentedControl failure mode again: written, working, left out of the barrel. The volume controls live in useSystemHardware — this is playback.

        Returns { play: (effect: SoundEffect) => void }

        • play: (effect: SoundEffect) => void

          Play one of the phone's built-in effects. Silent while muted.

    storage: (
        appId: string,
    ) => {
        clear: () => void;
        getItem: <T = unknown>(key: string, defaultValue?: T) => T | null;
        markUnsynced: (key: string) => void;
        removeItem: (key: string) => void;
        setItem: <T = unknown>(key: string, value: T) => void;
    }

    Type Declaration

      • (
            appId: string,
        ): {
            clear: () => void;
            getItem: <T = unknown>(key: string, defaultValue?: T) => T | null;
            markUnsynced: (key: string) => void;
            removeItem: (key: string) => void;
            setItem: <T = unknown>(key: string, value: T) => void;
        }
      • OS Service Hook for app key-value storage.

        Parameters

        • appId: string

        Returns {
            clear: () => void;
            getItem: <T = unknown>(key: string, defaultValue?: T) => T | null;
            markUnsynced: (key: string) => void;
            removeItem: (key: string) => void;
            setItem: <T = unknown>(key: string, value: T) => void;
        }

        • clear: () => void

          The wall-side route for clearAppStorage (GPHONE-16 step 4): that facet is a bare function, not a factory, so a remoteCall naming it has no member to call. This member is what the iframe twin's clearAppStorage(appId) actually reaches.

        • getItem: <T = unknown>(key: string, defaultValue?: T) => T | null
        • markUnsynced: (key: string) => void

          Serves the iframe persisted twin's markUnsynced (GPHONE-16 step 4): an add-on cannot import settingsSync directly, so this is the one member of the facet that reaches it on the add-on's behalf.

        • removeItem: (key: string) => void
        • setItem: <T = unknown>(key: string, value: T) => void
    systemHardware: () => {
        bluetoothEnabled: Writable<boolean>;
        cellServiceEnabled: Writable<boolean>;
        charge: Writable<number>;
        isBluetoothDiscoverable: Readable<boolean>;
        setSignal: (level: number) => void;
        setVolume: (val: number) => void;
        setVolumeStep: (percent: number) => void;
        signalLevel: Writable<number>;
        soundMuted: Writable<boolean>;
        soundVolume: Writable<number>;
        toggleBluetooth: () => void;
        toggleCellService: () => void;
        toggleMute: () => void;
        volumeStep: Writable<number>;
        volumeStepChoices: readonly [1, 2, 5, 10, 20];
    }

    Type Declaration

      • (): {
            bluetoothEnabled: Writable<boolean>;
            cellServiceEnabled: Writable<boolean>;
            charge: Writable<number>;
            isBluetoothDiscoverable: Readable<boolean>;
            setSignal: (level: number) => void;
            setVolume: (val: number) => void;
            setVolumeStep: (percent: number) => void;
            signalLevel: Writable<number>;
            soundMuted: Writable<boolean>;
            soundVolume: Writable<number>;
            toggleBluetooth: () => void;
            toggleCellService: () => void;
            toggleMute: () => void;
            volumeStep: Writable<number>;
            volumeStepChoices: readonly [1, 2, 5, 10, 20];
        }
      • The phone's hardware: battery, cellular signal, cell service, bluetooth, and volume controls.

        Returns {
            bluetoothEnabled: Writable<boolean>;
            cellServiceEnabled: Writable<boolean>;
            charge: Writable<number>;
            isBluetoothDiscoverable: Readable<boolean>;
            setSignal: (level: number) => void;
            setVolume: (val: number) => void;
            setVolumeStep: (percent: number) => void;
            signalLevel: Writable<number>;
            soundMuted: Writable<boolean>;
            soundVolume: Writable<number>;
            toggleBluetooth: () => void;
            toggleCellService: () => void;
            toggleMute: () => void;
            volumeStep: Writable<number>;
            volumeStepChoices: readonly [1, 2, 5, 10, 20];
        }

        • bluetoothEnabled: Writable<boolean>

          Bluetooth state management and anti-doxxing privacy controls.

          Defaults to ON (true) and persists state across app launches and phone restarts. When OFF, the device is invisible to proximity scans and blocks unsolicited contact sharing or snooping attempts.

        • cellServiceEnabled: Writable<boolean>

          Cellular network service state. Defaults to ON (true) and persists across reloads. When OFF, cell service is disabled and clamped signal level is forced to 0 bars.

        • charge: Writable<number>
        • isBluetoothDiscoverable: Readable<boolean>

          Derived store indicating whether the device is visible/discoverable for Bluetooth proximity features.

        • setSignal: (level: number) => void
        • setVolume: (val: number) => void
        • setVolumeStep: (percent: number) => void
        • signalLevel: Writable<number>
        • soundMuted: Writable<boolean>
        • soundVolume: Writable<number>
        • toggleBluetooth: () => void
        • toggleCellService: () => void
        • toggleMute: () => void
        • volumeStep: Writable<number>

          How far one physical volume-button press moves the volume, in whole percent.

        • volumeStepChoices: readonly [1, 2, 5, 10, 20]
    theme: () => {
        defaultTheme: ThemeState;
        isLightMode: Readable<boolean>;
        resetTheme: () => void;
        sanitizeSeed: (value: unknown) => string;
        schemeStore: Readable<M3Tokens>;
        seedFromRgbString: (value: string) => string | null;
        setThemeMode: (mode: ThemeMode) => void;
        setThemeSeed: (seed: string) => void;
        themeStore: Writable<ThemeState>;
    }

    Type Declaration

      • (): {
            defaultTheme: ThemeState;
            isLightMode: Readable<boolean>;
            resetTheme: () => void;
            sanitizeSeed: (value: unknown) => string;
            schemeStore: Readable<M3Tokens>;
            seedFromRgbString: (value: string) => string | null;
            setThemeMode: (mode: ThemeMode) => void;
            setThemeSeed: (seed: string) => void;
            themeStore: Writable<ThemeState>;
        }
      • Implementation of the useTheme facet — see the useTheme hook doc for the usage contract.

        Returns {
            defaultTheme: ThemeState;
            isLightMode: Readable<boolean>;
            resetTheme: () => void;
            sanitizeSeed: (value: unknown) => string;
            schemeStore: Readable<M3Tokens>;
            seedFromRgbString: (value: string) => string | null;
            setThemeMode: (mode: ThemeMode) => void;
            setThemeSeed: (seed: string) => void;
            themeStore: Writable<ThemeState>;
        }

        • defaultTheme: ThemeState
        • isLightMode: Readable<boolean>

          Whether the light scheme is showing, for a toggle to bind to.

        • resetTheme: () => void
        • sanitizeSeed: (value: unknown) => string
        • schemeStore: Readable<M3Tokens>

          The 47 resolved token values for the active seed and mode.

        • seedFromRgbString: (value: string) => string | null

          Convert an rgb()/rgba() string — what a color picker emits — into a seed.

        • setThemeMode: (mode: ThemeMode) => void
        • setThemeSeed: (seed: string) => void
        • themeStore: Writable<ThemeState>
    timer: () => {
        after: (ms: number, handler: () => void) => CancelTimer;
        clearAll: () => void;
        every: (ms: number, handler: () => void) => CancelTimer;
    }

    Type Declaration

      • (): {
            after: (ms: number, handler: () => void) => CancelTimer;
            clearAll: () => void;
            every: (ms: number, handler: () => void) => CancelTimer;
        }
      • Implementation of the useTimer facet. See the useTimer hook doc for the usage contract.

        Returns {
            after: (ms: number, handler: () => void) => CancelTimer;
            clearAll: () => void;
            every: (ms: number, handler: () => void) => CancelTimer;
        }

    wallpaper: () => {
        activeSeed: Readable<string>;
        backgroundForSeed: (seed: string, mode: ThemeMode) => string;
        defaultWallpaper: WallpaperState;
        presets: readonly WallpaperPreset[];
        resetWallpaper: () => void;
        seedFromImage: (source: string) => Promise<string | null>;
        setPresetWallpaper: (preset: WallpaperPreset) => void;
        setWallpaperImage: (image: string, seed?: string) => void;
        setWallpaperSeed: (seed: string) => void;
        wallpaperBackground: Readable<string>;
        wallpaperNeedsContrast: Readable<boolean>;
        wallpaperStore: Writable<WallpaperState>;
    }

    Type Declaration

      • (): {
            activeSeed: Readable<string>;
            backgroundForSeed: (seed: string, mode: ThemeMode) => string;
            defaultWallpaper: WallpaperState;
            presets: readonly WallpaperPreset[];
            resetWallpaper: () => void;
            seedFromImage: (source: string) => Promise<string | null>;
            setPresetWallpaper: (preset: WallpaperPreset) => void;
            setWallpaperImage: (image: string, seed?: string) => void;
            setWallpaperSeed: (seed: string) => void;
            wallpaperBackground: Readable<string>;
            wallpaperNeedsContrast: Readable<boolean>;
            wallpaperStore: Writable<WallpaperState>;
        }
      • Implementation of the useWallpaper facet — see the useWallpaper hook doc for the usage contract.

        Returns {
            activeSeed: Readable<string>;
            backgroundForSeed: (seed: string, mode: ThemeMode) => string;
            defaultWallpaper: WallpaperState;
            presets: readonly WallpaperPreset[];
            resetWallpaper: () => void;
            seedFromImage: (source: string) => Promise<string | null>;
            setPresetWallpaper: (preset: WallpaperPreset) => void;
            setWallpaperImage: (image: string, seed?: string) => void;
            setWallpaperSeed: (seed: string) => void;
            wallpaperBackground: Readable<string>;
            wallpaperNeedsContrast: Readable<boolean>;
            wallpaperStore: Writable<WallpaperState>;
        }

        • activeSeed: Readable<string>

          The seed in use, for a picker that wants to open on the current color.

        • backgroundForSeed: (seed: string, mode: ThemeMode) => string

          What a given seed would look like, without applying it.

          The swatch a preset button renders is the wallpaper that preset produces, generated the same way — so the button cannot advertise one thing and set another, which is exactly what a hand-written gradient beside a hand-written seed allowed.

        • defaultWallpaper: WallpaperState
        • presets: readonly WallpaperPreset[]
        • resetWallpaper: () => void
        • seedFromImage: (source: string) => Promise<string | null>
        • setPresetWallpaper: (preset: WallpaperPreset) => void
        • setWallpaperImage: (image: string, seed?: string) => void

          Use a photo, optionally re-seeding the theme from its dominant color.

          The seed is separate because a photo does not have to dictate the accent — and seedFromImage returns null when it cannot read one, in which case the picture changes and the colors stay put.

        • setWallpaperSeed: (seed: string) => void

          Pick a color: the wallpaper and every role in the phone follow it together.

        • wallpaperBackground: Readable<string>

          The CSS background for whatever is currently set.

          One value, one format, whichever kind of wallpaper it is — so PhoneFrame writes it to one property and never chooses between a class and a style.

        • wallpaperNeedsContrast: Readable<boolean>

          Whether text drawn over the wallpaper needs the .text-on-wallpaper treatment.

        • wallpaperStore: Writable<WallpaperState>

          The one preference that stays on this PC.

          Every other setting follows the player's citizenid to any machine. This one cannot affordably: a custom wallpaper is a base64 data URL of unbounded size, and syncing it would push megabytes across the NUI bridge and into MySQL every time the color changed.

          The part players actually notice does still follow them — the seed and light/dark mode live in themeStore, which syncs, and they are what generate the whole scheme. What stays behind is a photo you set from this machine's own gallery.