diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift index 180da203008..ec5b54aa8f1 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift @@ -141,7 +141,7 @@ private final class ComposerTextView: UITextView { replace(textRange, withText: "") } - private func loadImages(from providers: [NSItemProvider]) { + func loadImages(from providers: [NSItemProvider]) { let group = DispatchGroup() let lock = NSLock() var images = [UIImage?](repeating: nil, count: providers.count) @@ -282,7 +282,7 @@ private final class ComposerTextView: UITextView { } } -public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { +public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDropDelegate { private let textView = ComposerTextView() private let placeholderLabel = UILabel() private var value = "" @@ -326,6 +326,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { clipsToBounds = false textView.delegate = self + textView.textDropDelegate = self textView.backgroundColor = .clear textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 @@ -500,6 +501,41 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { return true } + public func textDroppableView( + _ textDroppableView: UIView & UITextDroppable, + proposalForDrop drop: UITextDropRequest + ) -> UITextDropProposal { + guard droppedImageProviders(in: drop) != nil else { + return drop.suggestedProposal + } + + // The composer owns image drops so UIKit does not insert NSTextAttachments + // that the controlled plain-text value cannot represent. + let proposal = UITextDropProposal(operation: .copy) + proposal.dropAction = .insert + proposal.dropPerformer = .delegate + return proposal + } + + public func textDroppableView( + _ textDroppableView: UIView & UITextDroppable, + willPerformDrop drop: UITextDropRequest + ) { + guard let imageProviders = droppedImageProviders(in: drop) else { + return + } + textView.loadImages(from: imageProviders) + } + + private func droppedImageProviders(in drop: UITextDropRequest) -> [NSItemProvider]? { + let providers = drop.dropSession.items.map(\.itemProvider) + guard !providers.isEmpty, + providers.allSatisfy({ $0.canLoadObject(ofClass: UIImage.self) }) else { + return nil + } + return providers + } + public func textViewDidBeginEditing(_ textView: UITextView) { onComposerFocus() } diff --git a/apps/mobile/package.json b/apps/mobile/package.json index eb8c84a2f5b..9a5e64aa46f 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -49,7 +49,7 @@ "@expo-google-fonts/dm-sans": "^0.4.2", "@expo/metro-runtime": "~56.0.15", "@expo/ui": "~56.0.18", - "@legendapp/list": "3.2.0", + "@legendapp/list": "3.3.3", "@noble/curves": "catalog:", "@noble/hashes": "catalog:", "@pierre/diffs": "catalog:", @@ -101,8 +101,8 @@ "expo-web-browser": "~56.0.5", "expo-widgets": "~56.0.19", "punycode": "^2.3.1", - "react": "19.2.6", - "react-dom": "19.2.6", + "react": "19.2.3", + "react-dom": "19.2.3", "react-native": "0.85.3", "react-native-gesture-handler": "~2.31.1", "react-native-image-viewing": "^0.2.2", diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 2dcfe3505f5..75f17136054 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -286,6 +286,15 @@ function workspacePathFromState(state: NavigationState): string { return path.startsWith("/") ? path : `/${path}`; } +// The drain hook subscribes to the outbox, all thread shells, projects, and +// connection statuses. Hosting it in a null-rendering leaf keeps those +// updates from re-rendering RootStackLayout (and with it every screen) on +// each enqueue, shell change, or reconnect. +function ThreadOutboxDrainWorker() { + useThreadOutboxDrain(); + return null; +} + function RootStackLayout(props: { readonly children: React.ReactNode; readonly state: NavigationState; @@ -294,7 +303,6 @@ function RootStackLayout(props: { const { pendingShare, revivedShareId, clearRevivedShareId } = useIncomingShare(); const sharePresentationRef = useRef(EMPTY_INCOMING_SHARE_PRESENTATION_STATE); useAgentNotificationNavigation(); - useThreadOutboxDrain(); // Presents the T3 Connect onboarding sheet after an in-session sign-in. useConnectOnboardingNavigation(); // Launcher app shortcuts: routes shortcut taps and tracks opened threads. @@ -326,6 +334,7 @@ function RootStackLayout(props: { return ( + {SHOWCASE_ENABLED ? : null} diff --git a/apps/mobile/src/connection/app-state-wakeups.test.ts b/apps/mobile/src/connection/app-state-wakeups.test.ts new file mode 100644 index 00000000000..4e8bdf1edf4 --- /dev/null +++ b/apps/mobile/src/connection/app-state-wakeups.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { + MOBILE_BACKGROUND_RECONNECT_AFTER_MS, + mobileApplicationActiveWakeup, +} from "./app-state-wakeups"; + +describe("mobileApplicationActiveWakeup", () => { + it("uses a fast probe after a short interruption", () => { + expect(mobileApplicationActiveWakeup(null, 20_000)).toBe("application-active-probe"); + expect( + mobileApplicationActiveWakeup(20_000, 20_000 + MOBILE_BACKGROUND_RECONNECT_AFTER_MS - 1), + ).toBe("application-active-probe"); + }); + + it("replaces the session after a meaningful background suspension", () => { + expect( + mobileApplicationActiveWakeup(20_000, 20_000 + MOBILE_BACKGROUND_RECONNECT_AFTER_MS), + ).toBe("application-active-reconnect"); + }); +}); diff --git a/apps/mobile/src/connection/app-state-wakeups.ts b/apps/mobile/src/connection/app-state-wakeups.ts new file mode 100644 index 00000000000..185ff744958 --- /dev/null +++ b/apps/mobile/src/connection/app-state-wakeups.ts @@ -0,0 +1,18 @@ +import type { Wakeups } from "@t3tools/client-runtime/connection"; + +export const MOBILE_BACKGROUND_RECONNECT_AFTER_MS = 10_000; + +export type MobileApplicationActiveWakeup = Extract< + Wakeups.ConnectionWakeup, + "application-active-probe" | "application-active-reconnect" +>; + +export function mobileApplicationActiveWakeup( + backgroundedAtMs: number | null, + activeAtMs: number, +): MobileApplicationActiveWakeup { + return backgroundedAtMs !== null && + activeAtMs - backgroundedAtMs >= MOBILE_BACKGROUND_RECONNECT_AFTER_MS + ? "application-active-reconnect" + : "application-active-probe"; +} diff --git a/apps/mobile/src/connection/platform.ts b/apps/mobile/src/connection/platform.ts index b8a13137c88..852535d9d10 100644 --- a/apps/mobile/src/connection/platform.ts +++ b/apps/mobile/src/connection/platform.ts @@ -30,6 +30,7 @@ import * as MobileStorage from "../persistence/mobile-storage"; import { appAtomRegistry } from "../state/atom-registry"; import { clearThreadOutboxEnvironment } from "../state/thread-outbox"; import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts"; +import { mobileApplicationActiveWakeup } from "./app-state-wakeups"; import { connectionStorageLayer } from "./storage"; function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "online" { @@ -54,27 +55,53 @@ const connectivityLayer = Connectivity.layer({ ), changes: Stream.callback((queue) => Effect.acquireRelease( - Effect.sync(() => - Network.addNetworkStateListener((state) => { + Effect.sync(() => { + let active = true; + const networkSubscription = Network.addNetworkStateListener((state) => { Queue.offerUnsafe(queue, networkStatus(state)); - }), - ), - (subscription) => Effect.sync(() => subscription.remove()), + }); + const appStateSubscription = AppState.addEventListener("change", (state) => { + if (state !== "active") { + return; + } + void Network.getNetworkStateAsync() + .then((current) => { + if (active) { + Queue.offerUnsafe(queue, networkStatus(current)); + } + }) + .catch(() => undefined); + }); + return { + close: () => { + active = false; + networkSubscription.remove(); + appStateSubscription.remove(); + }, + }; + }), + ({ close }) => Effect.sync(close), ).pipe(Effect.asVoid), ), }); const wakeupsLayer = Wakeups.layer({ changes: Stream.merge( - Stream.callback<"application-active">((queue) => + Stream.callback<"application-active-probe" | "application-active-reconnect">((queue) => Effect.acquireRelease( - Effect.sync(() => - AppState.addEventListener("change", (state) => { + Effect.sync(() => { + let backgroundedAtMs = AppState.currentState === "background" ? Date.now() : null; + return AppState.addEventListener("change", (state) => { + if (state === "background") { + backgroundedAtMs = Date.now(); + return; + } if (state === "active") { - Queue.offerUnsafe(queue, "application-active"); + Queue.offerUnsafe(queue, mobileApplicationActiveWakeup(backgroundedAtMs, Date.now())); + backgroundedAtMs = null; } - }), - ), + }); + }), (subscription) => Effect.sync(() => subscription.remove()), ).pipe(Effect.asVoid), ), diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 916802e9faf..01440007bc6 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -29,7 +29,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { @@ -70,7 +73,8 @@ function ArchivedThreadsHeader(props: { const searchIconColor = useThemeColor("--color-icon"); const searchTextColor = useThemeColor("--color-foreground"); const usesNativeChrome = Platform.OS === "ios"; - const usesCompactMailToolbar = Platform.OS === "ios" && width < 700; + const usesCompactMailToolbar = + Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; const androidFilterActions = useMemo( () => [ { @@ -272,7 +276,11 @@ function ArchivedThreadsHeader(props: { ...(usesNativeChrome ? { allowToolbarIntegration: true, - placement: "integratedButton" as const, + // "integratedButton" is an iOS 26 search-bar placement; + // pre-glass iOS keeps the default pull-down placement. + ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED + ? { placement: "integratedButton" as const } + : null), } : { placement: "stacked" as const, diff --git a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx index 608d43b2acb..173d093d849 100644 --- a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx +++ b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx @@ -24,13 +24,7 @@ import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironment import { ConnectionStatusDot } from "./ConnectionStatusDot"; import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController"; -/** - * "T3 Connect" section: every environment published to the signed-in account, - * with connect switches, availability status, refresh, and loading/error - * states. Shared between the Settings environments screen and the T3 Connect - * onboarding sheet. - */ -export function CloudEnvironmentRows(props: { +interface CloudEnvironmentRowsProps { readonly connectedCloudEnvironments: ReadonlyArray; readonly onReconnectEnvironment: (environmentId: EnvironmentId) => void; readonly showcaseAvailableEnvironments?: ReadonlyArray; @@ -41,8 +35,31 @@ export function CloudEnvironmentRows(props: { * pull-to-refresh). */ readonly showHeader?: boolean; -}) { +} + +/** + * "T3 Connect" section: every environment published to the signed-in account, + * with connect switches, availability status, refresh, and loading/error + * states. Shared between the Settings environments screen and the T3 Connect + * onboarding sheet. + */ +export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) { + // Showcase captures run without a Clerk publishable key, so `ClerkProvider` + // is never mounted and any `useAuth` call throws — the fixture states whether + // the rows are signed in instead of asking Clerk. + if (props.showcaseSignedIn !== undefined) { + return props.showcaseSignedIn ? : null; + } + return ; +} + +function SignedInCloudEnvironmentRows(props: CloudEnvironmentRowsProps) { const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); + if (!isSignedIn) return null; + return ; +} + +function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) { const controller = useConnectionController(); const iconColor = useThemeColor("--color-icon"); const availableCloudEnvironments = @@ -67,8 +84,6 @@ export function CloudEnvironmentRows(props: { const showHeader = props.showHeader ?? true; - if (!(props.showcaseSignedIn ?? isSignedIn)) return null; - return ( {showHeader ? ( diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index a9b1f59bb2f..f8c110b7fb5 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -29,7 +29,10 @@ import { useAdaptiveWorkspacePaneRole, useRegisterWorkspaceInspector, } from "../layout/AdaptiveWorkspaceLayout"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; import { ReviewHighlighterProvider } from "../review/ReviewHighlighterProvider"; import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; @@ -354,7 +357,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { ); } - const usesCompactMailToolbar = Platform.OS === "ios" && !layout.usesSplitView; + const usesCompactMailToolbar = + Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; return ( <> diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 4265107912b..12d5eaca473 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -13,7 +13,10 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { buildHomeListFilterMenu, @@ -320,9 +323,11 @@ function IosHomeHeader(props: HomeHeaderProps) { }), ] : undefined, - unstable_headerToolbarItems: - Platform.OS === "ios" - ? () => [ + // The keys below are set per-branch (not `undefined`) so a later + // reapply cannot clobber options owned by NativeHeaderToolbar. + ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED + ? { + unstable_headerToolbarItems: () => [ createNativeMailSearchToolbarItem({ composeButtonId: "home-new-task", composeSystemImageName: "square.and.pencil", @@ -336,14 +341,14 @@ function IosHomeHeader(props: HomeHeaderProps) { placeholder: "Search", searchTextChangeId: "home-search-text", }), - ] - : undefined, - headerSearchBarOptions: - Platform.OS === "ios" - ? undefined - : { + ], + } + : { + // Pre-Liquid-Glass iOS: standard pull-down search in the nav + // bar; create + sort live in the plain bottom toolbar below. + headerSearchBarOptions: { ref: searchBarRef, - allowToolbarIntegration: true, + autoCapitalize: "none" as const, hideNavigationBar: false, placeholder: "Search", onCancelButtonPress: () => { @@ -353,21 +358,11 @@ function IosHomeHeader(props: HomeHeaderProps) { props.onSearchQueryChange(event.nativeEvent.text); }, }, + }), }} /> - {Platform.OS === "ios" ? null : ( - - - - )} - - {Platform.OS === "ios" ? null : ( + {NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? null : ( ) : null} - - Sort projects - {PROJECT_SORT_OPTIONS.map((option) => ( - props.onProjectSortOrderChange(option.value)} - > - {option.label} - - ))} - + {threadListV2Enabled ? null : ( + + Sort projects + {PROJECT_SORT_OPTIONS.map((option) => ( + props.onProjectSortOrderChange(option.value)} + > + {option.label} + + ))} + + )} - - Sort threads - {THREAD_SORT_OPTIONS.map((option) => ( - props.onThreadSortOrderChange(option.value)} - > - {option.label} - - ))} - + {threadListV2Enabled ? null : ( + + Sort threads + {THREAD_SORT_OPTIONS.map((option) => ( + props.onThreadSortOrderChange(option.value)} + > + {option.label} + + ))} + + )} - - - + { + void checkForAppUpdateOnLaunch(); + }, []); + const { archiveThread, confirmDeleteThread, settleThread, unsettleThread } = useThreadListActions(); const pendingTasks = usePendingNewTasks(); diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 10158c17504..cd802f05130 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -36,12 +36,13 @@ import { ThreadListRow, ThreadListShowMoreRow, } from "../threads/thread-list-items"; -import { ThreadListV2Row } from "../threads/thread-list-v2-items"; +import { ThreadListV2PendingRow, ThreadListV2Row } from "../threads/thread-list-v2-items"; import { buildThreadListV2Items, + buildThreadListV2ListItems, THREAD_LIST_V2_SETTLED_INITIAL_COUNT, THREAD_LIST_V2_SETTLED_PAGE_COUNT, - type ThreadListV2Item, + type ThreadListV2ListItem, } from "../threads/threadListV2"; import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu"; import { @@ -101,6 +102,7 @@ interface HomeScreenProps { /* ─── Layout constants ───────────────────────────────────────────────── */ const ESTIMATED_THREAD_ROW_HEIGHT = 72; +const PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT = 44; /** * Top spacing between the list and the Android custom header. The Android * header (AndroidHomeHeader) is rendered in-flow above this screen and @@ -188,6 +190,10 @@ export function HomeScreen(props: HomeScreenProps) { const listRef = useRef(null); const insets = useSafeAreaInsets(); const accentColor = useThemeColor("--color-icon-muted"); + const iosBottomToolbarClearance = + Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED + ? PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT + : 0; const effectiveGroupDisplayStates = useMemo(() => { const next = new Map(groupDisplayStates); if (!AsyncResult.isSuccess(preferencesResult)) { @@ -553,50 +559,100 @@ export function HomeScreen(props: HomeScreenProps) { // unchanged: after a clamped fire (wake beyond the 32-bit setTimeout // range) the boundary string is identical and the chain would die. }, [nextSnoozeWakeAt, snoozeWakeTick]); - const threadListV2Items = threadListV2Layout.items; + // Queued tasks are not thread shells, so the v2 partition never sees them; + // they are spliced in below the active block and stay visible and deletable + // while their environment is offline. Same environment scope and search + // filter as the list itself. + const v2SearchQuery = props.searchQuery.trim().toLocaleLowerCase(); + const v2PendingTasks = useMemo( + () => + props.pendingTasks.filter( + (pendingTask) => + (props.selectedEnvironmentId === null || + pendingTask.message.environmentId === props.selectedEnvironmentId) && + (v2ScopedProjectKeys === null || + v2ScopedProjectKeys.has( + scopedProjectKey(pendingTask.message.environmentId, pendingTask.creation.projectId), + )) && + (v2SearchQuery.length === 0 || + pendingTask.title.toLocaleLowerCase().includes(v2SearchQuery)), + ), + [props.pendingTasks, props.selectedEnvironmentId, v2ScopedProjectKeys, v2SearchQuery], + ); + const threadListV2Items = useMemo( + () => + buildThreadListV2ListItems({ + items: threadListV2Layout.items, + pendingTasks: v2PendingTasks, + }), + [threadListV2Layout.items, v2PendingTasks], + ); const renderV2Item = useCallback( - ({ item }: { readonly item: ThreadListV2Item }) => ( - - provider.instanceId === - (item.thread.session?.providerInstanceId ?? item.thread.modelSelection.instanceId), - )?.driver ?? null - } - environmentLabel={ - Object.keys(props.savedConnectionsById).length > 1 - ? (props.savedConnectionsById[item.thread.environmentId]?.environmentLabel ?? null) - : null - } - onSelectThread={props.onSelectThread} - onDeleteThread={handleDeleteThread} - onArchiveThread={props.onArchiveThread} - settlementSupported={settlementEnvironmentIds.has(item.thread.environmentId)} - onSettleThread={handleSettleThread} - onUnsettleThread={handleUnsettleThread} - onChangeRequestState={handleChangeRequestState} - projectCwd={ - projectCwdByKey.get(scopedProjectKey(item.thread.environmentId, item.thread.projectId)) ?? - null - } - onSwipeableClose={handleSwipeableClose} - onSwipeableWillOpen={handleSwipeableWillOpen} - /> - ), + ({ item }: { readonly item: ThreadListV2ListItem }) => { + if (item.type === "v2-pending") { + const pendingScopeKey = scopedProjectKey( + item.pendingTask.message.environmentId, + item.pendingTask.creation.projectId, + ); + return ( + 1 + ? (props.savedConnectionsById[item.pendingTask.message.environmentId] + ?.environmentLabel ?? null) + : null + } + showPendingDivider={item.showPendingDivider} + onSelectPendingTask={props.onSelectPendingTask} + onDeletePendingTask={props.onDeletePendingTask} + /> + ); + } + const thread = item.item.thread; + return ( + + provider.instanceId === + (thread.session?.providerInstanceId ?? thread.modelSelection.instanceId), + )?.driver ?? null + } + environmentLabel={ + Object.keys(props.savedConnectionsById).length > 1 + ? (props.savedConnectionsById[thread.environmentId]?.environmentLabel ?? null) + : null + } + onSelectThread={props.onSelectThread} + onDeleteThread={handleDeleteThread} + onArchiveThread={props.onArchiveThread} + settlementSupported={settlementEnvironmentIds.has(thread.environmentId)} + onSettleThread={handleSettleThread} + onUnsettleThread={handleUnsettleThread} + onChangeRequestState={handleChangeRequestState} + projectCwd={ + projectCwdByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null + } + onSwipeableClose={handleSwipeableClose} + onSwipeableWillOpen={handleSwipeableWillOpen} + /> + ); + }, [ handleChangeRequestState, handleDeleteThread, @@ -607,6 +663,8 @@ export function HomeScreen(props: HomeScreenProps) { projectByKey, projectCwdByKey, props.onArchiveThread, + props.onDeletePendingTask, + props.onSelectPendingTask, props.onSelectThread, props.savedConnectionsById, serverConfigs, @@ -614,9 +672,26 @@ export function HomeScreen(props: HomeScreenProps) { v2ProjectTitleByProjectKey, ], ); - const v2KeyExtractor = useCallback( - (item: ThreadListV2Item) => `${item.thread.environmentId}:${item.thread.id}`, - [], + const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []); + + // FlatList treats a changed extraData identity as "re-render every visible + // row", so an inline object literal would invalidate all rows on every + // HomeScreen render. + const v2ExtraData = useMemo( + () => ({ + projectByKey, + projectCwdByKey, + projectTitleByProjectKey: v2ProjectTitleByProjectKey, + serverConfigs, + savedConnectionsById: props.savedConnectionsById, + }), + [ + projectByKey, + projectCwdByKey, + props.savedConnectionsById, + serverConfigs, + v2ProjectTitleByProjectKey, + ], ); const extraData = useMemo( @@ -744,7 +819,7 @@ export function HomeScreen(props: HomeScreenProps) { @@ -792,41 +867,9 @@ export function HomeScreen(props: HomeScreenProps) { ); - // v2 renders queued offline tasks above the thread cards — they are not - // thread shells, so the v2 item builder never sees them, but they must - // stay visible and deletable while their environment is offline. They - // respect the same environment scope and search filter as the list. - const v2SearchQuery = props.searchQuery.trim().toLocaleLowerCase(); - const v2PendingTasks = props.pendingTasks.filter( - (pendingTask) => - (props.selectedEnvironmentId === null || - pendingTask.message.environmentId === props.selectedEnvironmentId) && - (v2ScopedProjectKeys === null || - v2ScopedProjectKeys.has( - scopedProjectKey(pendingTask.message.environmentId, pendingTask.creation.projectId), - )) && - (v2SearchQuery.length === 0 || pendingTask.title.toLocaleLowerCase().includes(v2SearchQuery)), - ); // Project scoping lives in the header filter menu (no inline chip row on // mobile — the menu is the one filter surface). - const v2ListHeader = ( - <> - {listHeader} - {v2PendingTasks.map((pendingTask, index) => ( - - ))} - - ); + const v2ListHeader = listHeader; const listEmpty = !hasResults ? ( hasSearchQuery ? ( @@ -850,37 +893,33 @@ export function HomeScreen(props: HomeScreenProps) { // is empty. Search outranks the scope — "No results" names the actionable // fact when a query is active. Snoozed threads outrank the rest: "No // threads yet" over an inbox that is merely all-snoozed reads as data - // loss. Pending tasks render in the header, so the list showing them - // isn't empty in the user's eyes. + // loss. const v2SnoozedCount = threadListV2Layout.snoozedCount; - const v2ListEmpty = - v2PendingTasks.length > 0 ? null : hasSearchQuery ? ( - v2SnoozedCount > 0 ? ( - // The snoozed threads already passed this search filter: "No - // results" would claim nothing matched when matches are merely - // parked. - - ) : ( - - ) - ) : v2SnoozedCount > 0 ? ( + const v2ListEmpty = hasSearchQuery ? ( + v2SnoozedCount > 0 ? ( + // The snoozed threads already passed this search filter: "No + // results" would claim nothing matched when matches are merely + // parked. - ) : v2ScopedProjectGroup !== null ? ( - ) : ( - listEmpty - ); + + ) + ) : v2SnoozedCount > 0 ? ( + + ) : v2ScopedProjectGroup !== null ? ( + + ) : ( + listEmpty + ); if (threadListV2Enabled) { return ( @@ -890,11 +929,7 @@ export function HomeScreen(props: HomeScreenProps) { data={threadListV2Items} renderItem={renderV2Item} keyExtractor={v2KeyExtractor} - extraData={{ - projectByKey, - serverConfigs, - savedConnectionsById: props.savedConnectionsById, - }} + extraData={v2ExtraData} ListHeaderComponent={v2ListHeader} ListFooterComponent={ threadListV2Layout.hiddenSettledCount > 0 ? ( @@ -923,7 +958,7 @@ export function HomeScreen(props: HomeScreenProps) { contentContainerStyle={{ paddingBottom: Platform.OS === "ios" - ? Math.max(insets.bottom, 24) + 96 + ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance : Math.max(insets.bottom, 16) + 88, }} /> @@ -964,16 +999,19 @@ export function HomeScreen(props: HomeScreenProps) { scrollEventThrottle={16} contentContainerStyle={{ // Android reserves room for the floating new-task FAB - // (56 button + 16 gap + bottom inset). + // (56 button + 16 gap + bottom inset). Pre-glass iOS shows a + // standard 44pt bottom toolbar that overlays the list and is not + // reflected in insets while contentInsetAdjustmentBehavior is + // "never". paddingBottom: Platform.OS === "ios" - ? Math.max(insets.bottom, 24) + 24 + ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance : Math.max(insets.bottom, 16) + 88, }} scrollIndicatorInsets={ Platform.OS === "ios" ? { - bottom: Math.max(insets.bottom, 16) + 24, + bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance, top: 0, } : undefined diff --git a/apps/mobile/src/features/layout/native-mail-search-toolbar.ts b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts index 820e1222243..8770d96b124 100644 --- a/apps/mobile/src/features/layout/native-mail-search-toolbar.ts +++ b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts @@ -1,5 +1,16 @@ import type { HeaderBarButtonMailSearchToolbarItem } from "react-native-screens"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; + +/** + * The patched mail-style toolbar is built natively from iOS 26 Liquid Glass + * UIKit (`UIGlassEffect`) with no earlier fallback: pre-26 the native side + * silently drops the item and hides the navigation toolbar entirely. Screens + * that send it must fall back to standard search/toolbar primitives when this + * is false. + */ +export const NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED = NATIVE_LIQUID_GLASS_SUPPORTED; + type NativeMailSearchToolbarInput = Omit< HeaderBarButtonMailSearchToolbarItem, "type" | "useFallbackSearchField" diff --git a/apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts b/apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts new file mode 100644 index 00000000000..3464bfda260 --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts @@ -0,0 +1,41 @@ +import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; +import { EnvironmentId } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic"; + +const ENVIRONMENT_A = EnvironmentId.make("environment-a"); +const ENVIRONMENT_B = EnvironmentId.make("environment-b"); + +function environment(environmentId: EnvironmentId, connectionState: EnvironmentConnectionPhase) { + return { environmentId, connectionState }; +} + +describe("resolveAddProjectEnvironment", () => { + it("does not redirect an explicit unavailable environment to another environment", () => { + expect( + resolveAddProjectEnvironment( + [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")], + ENVIRONMENT_A, + ), + ).toBeNull(); + }); + + it("resolves an explicit connected environment", () => { + expect( + resolveAddProjectEnvironment( + [environment(ENVIRONMENT_A, "connected"), environment(ENVIRONMENT_B, "connected")], + ENVIRONMENT_A, + )?.environmentId, + ).toBe(ENVIRONMENT_A); + }); + + it("defaults to the first connected environment when no environment is requested", () => { + expect( + resolveAddProjectEnvironment( + [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")], + null, + )?.environmentId, + ).toBe(ENVIRONMENT_B); + }); +}); diff --git a/apps/mobile/src/features/projects/AddProjectScreen.logic.ts b/apps/mobile/src/features/projects/AddProjectScreen.logic.ts new file mode 100644 index 00000000000..b208a1719ab --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectScreen.logic.ts @@ -0,0 +1,26 @@ +import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects"; +import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; +import type { EnvironmentId } from "@t3tools/contracts"; + +export function resolveAddProjectEnvironment< + T extends { + readonly environmentId: EnvironmentId; + readonly connectionState: EnvironmentConnectionPhase; + }, +>(environmentOptions: ReadonlyArray, requestedEnvironmentId: EnvironmentId | null): T | null { + if (requestedEnvironmentId !== null) { + return ( + environmentOptions.find( + (environment) => + environment.environmentId === requestedEnvironmentId && + canCreateProjectInEnvironment(environment.connectionState), + ) ?? null + ); + } + + return ( + environmentOptions.find((environment) => + canCreateProjectInEnvironment(environment.connectionState), + ) ?? null + ); +} diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index e657f3db6a9..7da57b919b8 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -4,27 +4,32 @@ import { addProjectRemoteSourceProvider, buildAddProjectRemoteSourceReadiness, buildProjectCreateCommand, + canCreateProjectInEnvironment, findExistingAddProject, getAddProjectInitialQuery, resolveAddProjectPath, sortAddProjectProviderSources, type AddProjectRemoteSource, } from "@t3tools/client-runtime/operations/projects"; +import { + connectionStatusText, + type EnvironmentConnectionPhase, +} from "@t3tools/client-runtime/connection"; +import { + canPreloadBrowsePath, + createBrowseNavigationCoordinator, + filterFilesystemBrowseEntries, + getFilesystemBrowsePath, +} from "@t3tools/client-runtime/state/filesystem"; import { appendBrowsePathSegment, - canNavigateUp, ensureBrowseDirectoryPath, - getBrowseDirectoryPath, - getBrowseLeafPathSegment, - getBrowseParentPath, - hasTrailingPathSeparator, inferProjectTitleFromPath, - isFilesystemBrowseQuery, } from "@t3tools/client-runtime/state/projects"; import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts"; import { StackActions, useNavigation } from "@react-navigation/native"; import { SymbolView } from "../../components/AppSymbol"; -import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import * as Arr from "effect/Array"; @@ -45,13 +50,21 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { uuidv4 } from "../../lib/uuid"; import { useAtomCommand } from "../../state/use-atom-command"; import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; -import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; +import { + useRemoteConnectionStatus, + useRemoteEnvironmentRuntime, + useSavedRemoteConnections, +} from "../../state/use-remote-environment-registry"; +import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic"; interface EnvironmentOption { readonly environmentId: EnvironmentId; readonly label: string; readonly platform: string; readonly baseDirectory: string | null; + readonly connectionState: EnvironmentConnectionPhase; + readonly connectionError: string | null; + readonly connectionErrorTraceId: string | null; } const environmentOptionOrder = Order.mapInput( @@ -61,11 +74,6 @@ const environmentOptionOrder = Order.mapInput( (environment: EnvironmentOption) => ({ label: environment.label }), ); -const browseEntryOrder = Order.mapInput( - Order.String, - (entry: { readonly name: string }) => entry.name, -); - function platformFromOs(os: string | null | undefined): string { if (os === "windows") return "Win32"; if (os === "darwin") return "MacIntel"; @@ -228,22 +236,91 @@ function ProjectPathInput(props: { ); } +function useBrowsePathInput(environment: EnvironmentOption | null) { + const environmentId = environment?.environmentId ?? null; + const environmentBaseDirectory = environment?.baseDirectory ?? null; + const [pathInput, commitPathInput] = useState(() => + getAddProjectInitialQuery(environmentBaseDirectory), + ); + const previousEnvironmentIdRef = useRef(environmentId); + const environmentRuntime = useRemoteEnvironmentRuntime(environmentId); + const loadBrowsePath = useAtomQueryRunner(filesystemEnvironment.browse, { + reportFailure: false, + reportDefect: false, + }); + const [browseNavigation] = useState(createBrowseNavigationCoordinator); + const [isBrowseNavigating, setIsBrowseNavigating] = useState(false); + const setPathInput = useCallback( + (path: string) => { + browseNavigation.invalidate(); + setIsBrowseNavigating(false); + commitPathInput(path); + }, + [browseNavigation], + ); + const navigateToBrowsePath = useCallback( + async (path: string) => { + setIsBrowseNavigating(true); + const committed = await browseNavigation.run( + async () => { + if (environment && canPreloadBrowsePath(environmentRuntime?.connectionState)) { + await loadBrowsePath({ + environmentId: environment.environmentId, + input: { partialPath: path }, + }); + } + }, + () => commitPathInput(path), + ); + if (committed) { + setIsBrowseNavigating(false); + } + return committed; + }, + [browseNavigation, environment, environmentRuntime?.connectionState, loadBrowsePath], + ); + + useEffect(() => { + if (environmentId !== null && environmentId !== previousEnvironmentIdRef.current) { + previousEnvironmentIdRef.current = environmentId; + setPathInput(getAddProjectInitialQuery(environmentBaseDirectory)); + } + }, [environmentBaseDirectory, environmentId, setPathInput]); + + useEffect( + () => () => { + browseNavigation.invalidate(); + }, + [browseNavigation], + ); + + return { isBrowseNavigating, pathInput, setPathInput, navigateToBrowsePath }; +} + function useEnvironmentOptions(): ReadonlyArray { const serverConfigByEnvironmentId = useServerConfigs(); const { savedConnectionsById } = useSavedRemoteConnections(); + const { connectedEnvironments } = useRemoteConnectionStatus(); return useMemo>(() => { + const runtimeByEnvironmentId = new Map( + connectedEnvironments.map((environment) => [environment.environmentId, environment] as const), + ); const options = Object.values(savedConnectionsById).map((connection) => { const config = serverConfigByEnvironmentId.get(connection.environmentId); + const runtime = runtimeByEnvironmentId.get(connection.environmentId); return { environmentId: connection.environmentId, label: connection.environmentLabel, platform: platformFromOs(config?.environment.platform.os ?? null), baseDirectory: config?.settings.addProjectBaseDirectory ?? null, + connectionState: runtime?.connectionState ?? "available", + connectionError: runtime?.connectionError ?? null, + connectionErrorTraceId: runtime?.connectionErrorTraceId ?? null, }; }); return Arr.sort(options, environmentOptionOrder); - }, [savedConnectionsById, serverConfigByEnvironmentId]); + }, [connectedEnvironments, savedConnectionsById, serverConfigByEnvironmentId]); } function useSelectedEnvironment(): { @@ -254,8 +331,14 @@ function useSelectedEnvironment(): { const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(null); const environmentOptions = useEnvironmentOptions(); const selectedEnvironment = - environmentOptions.find((environment) => environment.environmentId === selectedEnvironmentId) ?? - environmentOptions[0] ?? + environmentOptions.find( + (environment) => + environment.environmentId === selectedEnvironmentId && + canCreateProjectInEnvironment(environment.connectionState), + ) ?? + environmentOptions.find((environment) => + canCreateProjectInEnvironment(environment.connectionState), + ) ?? null; return { @@ -270,9 +353,9 @@ function EmptyEnvironmentState() { return ( - No environments connected + Environment unavailable - Add an environment before adding a project. + Start or reconnect an environment before adding a project. navigation.dispatch(StackActions.replace("ConnectionsNew"))} @@ -355,17 +438,25 @@ export function AddProjectSourceScreen(props: { readonly incomingShareId?: strin return ( - {environmentOptions.length === 0 ? : null} + {selectedEnvironment === null ? : null} {environmentOptions.length > 1 ? ( <> - Connected environments + Environments {environmentOptions.map((environment, index) => ( } selected={environment.environmentId === selectedEnvironment?.environmentId} + disabled={!canCreateProjectInEnvironment(environment.connectionState)} isFirst={index === 0} right={ environment.environmentId === selectedEnvironment?.environmentId ? ( @@ -450,7 +542,7 @@ function useCreateProject(environment: EnvironmentOption | null, incomingShareId return useCallback( async (workspaceRoot: string) => { - if (!environment) return; + if (!environment || !canCreateProjectInEnvironment(environment.connectionState)) return; const existing = findExistingAddProject({ projects, @@ -503,11 +595,7 @@ function useEnvironmentFromParam( ): EnvironmentOption | null { const environmentOptions = useEnvironmentOptions(); const environmentId = stringParam(environmentIdParam) as EnvironmentId | null; - return ( - environmentOptions.find((environment) => environment.environmentId === environmentId) ?? - environmentOptions[0] ?? - null - ); + return resolveAddProjectEnvironment(environmentOptions, environmentId); } export function AddProjectRepositoryScreen(props: { @@ -583,26 +671,32 @@ export function AddProjectRepositoryScreen(props: { return ( {error ? : null} - void lookupRepository()} - /> - void lookupRepository()} - loading={isSubmitting} - /> + {environment ? ( + <> + void lookupRepository()} + /> + void lookupRepository()} + loading={isSubmitting} + /> + + ) : ( + + )} ); } @@ -611,22 +705,16 @@ function FolderBrowser(props: { readonly environment: EnvironmentOption; readonly pathInput: string; readonly setPathInput: (path: string) => void; + readonly navigateToBrowsePath: (path: string) => Promise; }) { const accentColor = useThemeColor("--color-icon-muted"); - const browseDirectoryPath = useMemo( - () => - isFilesystemBrowseQuery(props.pathInput, props.environment.platform) - ? getBrowseDirectoryPath(props.pathInput) - : "", + const browsePath = useMemo( + () => getFilesystemBrowsePath(props.pathInput, props.environment.platform), [props.environment.platform, props.pathInput], ); - const browseFilterQuery = - browseDirectoryPath.length > 0 && !hasTrailingPathSeparator(props.pathInput) - ? getBrowseLeafPathSegment(props.pathInput).toLowerCase() - : ""; const browseInput = useMemo( - () => (browseDirectoryPath.length > 0 ? { partialPath: browseDirectoryPath } : null), - [browseDirectoryPath], + () => (browsePath.directoryPath.length > 0 ? { partialPath: browsePath.directoryPath } : null), + [browsePath.directoryPath], ); const browseState = useEnvironmentQuery( browseInput === null @@ -636,20 +724,10 @@ function FolderBrowser(props: { input: browseInput, }), ); - const visibleBrowseEntries = useMemo( - () => - Arr.sort( - Arr.filter( - browseState.data?.entries ?? [], - (entry) => - !entry.name.startsWith(".") && entry.name.toLowerCase().startsWith(browseFilterQuery), - ), - browseEntryOrder, - ), - [browseFilterQuery, browseState.data?.entries], + const { visibleEntries: visibleBrowseEntries } = useMemo( + () => filterFilesystemBrowseEntries(browseState.data?.entries ?? [], browsePath.filterQuery), + [browsePath.filterQuery, browseState.data?.entries], ); - const parentBrowsePath = getBrowseParentPath(browseDirectoryPath); - const canBrowseUpPath = canNavigateUp(browseDirectoryPath); return ( <> @@ -661,7 +739,7 @@ function FolderBrowser(props: { ) : null} - {canBrowseUpPath ? ( + {browsePath.canBrowseUp ? ( { - if (parentBrowsePath) props.setPathInput(parentBrowsePath); + if (browsePath.parentPath) { + void props.navigateToBrowsePath(browsePath.parentPath); + } }} /> ) : null} @@ -684,15 +764,15 @@ function FolderBrowser(props: { key={entry.fullPath} title={entry.name} icon={} - isFirst={index === 0 && !canBrowseUpPath} + isFirst={index === 0 && !browsePath.canBrowseUp} right={null} - onPress={() => - props.setPathInput( - browseDirectoryPath.length > 0 - ? appendBrowsePathSegment(browseDirectoryPath, entry.name) - : ensureBrowseDirectoryPath(entry.fullPath), - ) - } + onPress={() => { + const nextPath = + browsePath.directoryPath.length > 0 + ? appendBrowsePathSegment(browsePath.directoryPath, entry.name) + : ensureBrowseDirectoryPath(entry.fullPath); + void props.navigateToBrowsePath(nextPath); + }} /> ))} @@ -706,19 +786,13 @@ export function AddProjectLocalFolderScreen(props: { }) { const environment = useEnvironmentFromParam(props.environmentId); const createProject = useCreateProject(environment, stringParam(props.incomingShareId)); - const [pathInput, setPathInput] = useState(() => - getAddProjectInitialQuery(environment?.baseDirectory), - ); + const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } = + useBrowsePathInput(environment); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - if (!environment) return; - setPathInput(getAddProjectInitialQuery(environment.baseDirectory)); - }, [environment]); - const submitPath = useCallback(async () => { - if (!environment || isSubmitting) return; + if (!environment || isBrowseNavigating || isSubmitting) return; setError(null); const resolved = resolveAddProjectPath({ rawPath: pathInput, @@ -736,7 +810,7 @@ export function AddProjectLocalFolderScreen(props: { setError(errorMessage(Cause.squash(result.cause))); } setIsSubmitting(false); - }, [createProject, environment, isSubmitting, pathInput]); + }, [createProject, environment, isBrowseNavigating, isSubmitting, pathInput]); return ( @@ -750,12 +824,13 @@ export function AddProjectLocalFolderScreen(props: { /> void submitPath()} loading={isSubmitting} /> @@ -780,19 +855,13 @@ export function AddProjectDestinationScreen(props: { const createProject = useCreateProject(environment, stringParam(props.incomingShareId)); const remoteUrl = stringParam(props.remoteUrl); const repositoryTitle = stringParam(props.repositoryTitle); - const [pathInput, setPathInput] = useState(() => - getAddProjectInitialQuery(environment?.baseDirectory), - ); + const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } = + useBrowsePathInput(environment); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - if (!environment) return; - setPathInput(getAddProjectInitialQuery(environment.baseDirectory)); - }, [environment]); - const submitPath = useCallback(async () => { - if (!environment || !remoteUrl || isSubmitting) return; + if (!environment || !remoteUrl || isBrowseNavigating || isSubmitting) return; setError(null); const resolved = resolveAddProjectPath({ rawPath: pathInput, @@ -821,7 +890,15 @@ export function AddProjectDestinationScreen(props: { } } setIsSubmitting(false); - }, [cloneRepository, createProject, environment, isSubmitting, pathInput, remoteUrl]); + }, [ + cloneRepository, + createProject, + environment, + isBrowseNavigating, + isSubmitting, + pathInput, + remoteUrl, + ]); return ( @@ -843,12 +920,13 @@ export function AddProjectDestinationScreen(props: { /> void submitPath()} loading={isSubmitting} /> diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 0d23b890ad0..49adfe75cb2 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -8,8 +8,8 @@ import { NativeStackScreenOptions } from "../../native/StackHeader"; import { SymbolView } from "../../components/AppSymbol"; import * as Effect from "effect/Effect"; import { AsyncResult } from "effect/unstable/reactivity"; -import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react"; -import { Alert, Linking, Platform, ScrollView, View } from "react-native"; +import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; +import { Alert, Linking, Platform, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { @@ -38,6 +38,11 @@ import { runtime } from "../../lib/runtime"; import { useThemeColor } from "../../lib/useThemeColor"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; +import { + type AppUpdateCheckState, + registerHiddenUpdateTap, + runAppUpdateCheck, +} from "../updates/app-updates"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; import { SettingsRow } from "./components/SettingsRow"; import { SettingsSection } from "./components/SettingsSection"; @@ -157,7 +162,7 @@ function ConfiguredSettingsRouteScreen() { const environmentCount = connections.length; const accountLabel = useMemo(() => { if (!isLoaded) return "Checking"; - if (!isSignedIn) return "Request access"; + if (!isSignedIn) return "Sign in"; return user?.primaryEmailAddress?.emailAddress ?? "Signed in"; }, [isLoaded, isSignedIn, user?.primaryEmailAddress?.emailAddress]); @@ -571,6 +576,9 @@ function BetaSettingsSection() { function AppSettingsSection() { const icon = useThemeColor("--color-icon"); + const [updateState, setUpdateState] = useState("idle"); + const updateInFlight = useRef(false); + const hiddenUpdateTapCount = useRef(0); const version = Constants.expoConfig?.version ?? "0.0.0"; // Fall back to "production" to match resolveAppVariant in app.config.ts, so a @@ -578,37 +586,87 @@ function AppSettingsSection() { const variant = (Constants.expoConfig?.extra?.appVariant as string | undefined) ?? "production"; const variantLabel = variant === "production" ? "" : capitalize(variant); const versionLabel = variantLabel ? `${version} · ${variantLabel}` : version; - // Which JS is actually running: the bundle shipped in the binary, or an OTA - // update downloaded on top of it. Surfacing this makes "am I even on the - // right build?" answerable at a glance. - const bundleLabel = Updates.isEnabled - ? Updates.isEmbeddedLaunch - ? "Embedded" - : Updates.updateId - ? `OTA ${Updates.updateId.slice(0, 7)}` - : null - : null; + const busy = + updateState === "checking" || updateState === "downloading" || updateState === "restarting"; + + // "Up to date" is a transient acknowledgement, not a state worth persisting — + // return the version row to its normal, deliberately quiet state. + useEffect(() => { + if (updateState !== "current") return; + const timer = setTimeout(() => setUpdateState("idle"), 3000); + return () => clearTimeout(timer); + }, [updateState]); + + const checkForUpdate = useCallback(async () => { + // `disabled={busy}` only takes effect on the next render, so two taps in the + // same frame would both get through. The ref closes that window. + if (updateInFlight.current) return; + updateInFlight.current = true; + try { + await runAppUpdateCheck({ + onFailure: (message) => Alert.alert("Update failed", message), + onStateChange: setUpdateState, + }); + } finally { + updateInFlight.current = false; + } + }, []); + + const handleVersionPress = useCallback(() => { + if (!Updates.isEnabled || updateInFlight.current) return; + const tap = registerHiddenUpdateTap(hiddenUpdateTapCount.current); + hiddenUpdateTapCount.current = tap.nextCount; + if (tap.shouldCheck) { + void checkForUpdate(); + } + }, [checkForUpdate]); + + const statusLabel = + updateState === "checking" + ? "Checking…" + : updateState === "downloading" + ? "Downloading…" + : updateState === "restarting" + ? "Restarting…" + : updateState === "current" + ? "Up to date" + : null; + + const versionRow = ( + + + Version + + {versionLabel} + {statusLabel ? ( + {statusLabel} + ) : null} + + + ); return ( - - - Version - - {versionLabel} - {bundleLabel ? ( - {bundleLabel} - ) : null} - - + {Updates.isEnabled ? ( + + {versionRow} + + ) : ( + versionRow + )} ); } diff --git a/apps/mobile/src/features/sharing/IncomingShareProvider.tsx b/apps/mobile/src/features/sharing/IncomingShareProvider.tsx index e188336c0fc..353aea28ad0 100644 --- a/apps/mobile/src/features/sharing/IncomingShareProvider.tsx +++ b/apps/mobile/src/features/sharing/IncomingShareProvider.tsx @@ -16,6 +16,7 @@ import { type IncomingShareDestination, type IncomingShareDraft, } from "./incoming-share-model"; +import { createIncomingSharePayloadReader } from "./incoming-share-native"; import { IncomingShareInbox } from "./incoming-share-inbox"; import { loadIncomingShareDrafts, @@ -51,6 +52,11 @@ function receiveSharingEnabled(): boolean { return Constants.expoConfig?.extra?.iosPersonalTeamBuild !== true; } +const getIncomingSharePayloads = createIncomingSharePayloadReader({ + platform: Platform.OS, + readPayloads: getSharedPayloads, +}); + async function resolvedPayloadsForImages(): Promise> { try { return await getResolvedSharedPayloadsAsync(); @@ -124,7 +130,7 @@ const incomingShareInbox = new IncomingShareInbox({ loadDrafts: loadIncomingShareDrafts, writeDraft: writeIncomingShareDraft, removeDraft: removeIncomingShareDraft, - getPayloads: getSharedPayloads, + getPayloads: getIncomingSharePayloads, clearPayloads: clearSharedPayloads, buildDraft: async ({ payloads, id, createdAt }) => { const cleanupUris = new Set(); diff --git a/apps/mobile/src/features/sharing/incoming-share-native.test.ts b/apps/mobile/src/features/sharing/incoming-share-native.test.ts new file mode 100644 index 00000000000..8511f61ffa2 --- /dev/null +++ b/apps/mobile/src/features/sharing/incoming-share-native.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from "@effect/vitest"; +import type { SharePayload } from "expo-sharing"; + +import { createIncomingSharePayloadReader } from "./incoming-share-native"; + +const PAYLOAD: SharePayload = { + shareType: "text", + mimeType: "text/plain", + value: "Fix this", +}; + +describe("createIncomingSharePayloadReader", () => { + it("treats a missing iOS App Group as an unavailable inbox", () => { + const readPayloads = vi.fn((): ReadonlyArray => { + throw Object.assign(new Error("Expo-sharing has failed to fetch the app group id"), { + code: "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID", + }); + }); + const read = createIncomingSharePayloadReader({ platform: "ios", readPayloads }); + + expect(read()).toEqual([]); + expect(read()).toEqual([]); + expect(readPayloads).toHaveBeenCalledTimes(1); + }); + + it("returns native payloads when receiving is available", () => { + const read = createIncomingSharePayloadReader({ + platform: "ios", + readPayloads: () => [PAYLOAD], + }); + + expect(read()).toEqual([PAYLOAD]); + }); + + it("preserves other native errors", () => { + const error = new Error("native failure"); + const read = createIncomingSharePayloadReader({ + platform: "ios", + readPayloads: () => { + throw error; + }, + }); + + expect(read).toThrow(error); + }); +}); diff --git a/apps/mobile/src/features/sharing/incoming-share-native.ts b/apps/mobile/src/features/sharing/incoming-share-native.ts new file mode 100644 index 00000000000..b6da9534c92 --- /dev/null +++ b/apps/mobile/src/features/sharing/incoming-share-native.ts @@ -0,0 +1,37 @@ +import type { SharePayload } from "expo-sharing"; + +const IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE = "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID"; + +function errorCode(error: unknown): string | null { + if (typeof error !== "object" || error === null || !("code" in error)) { + return null; + } + return typeof error.code === "string" ? error.code : null; +} + +/** + * Normalizes the native "share into" capability to an empty inbox. Personal + * Team builds cannot include the App Group that expo-sharing reads from. + */ +export function createIncomingSharePayloadReader(input: { + readonly platform: string; + readonly readPayloads: () => ReadonlyArray; +}): () => ReadonlyArray { + let isUnavailable = false; + + return () => { + if (isUnavailable) { + return []; + } + + try { + return input.readPayloads(); + } catch (error) { + if (input.platform === "ios" && errorCode(error) === IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE) { + isUnavailable = true; + return []; + } + throw error; + } + }; +} diff --git a/apps/mobile/src/features/shortcuts/useAppShortcuts.ts b/apps/mobile/src/features/shortcuts/useAppShortcuts.ts index 3070a1d54e0..30b4e169889 100644 --- a/apps/mobile/src/features/shortcuts/useAppShortcuts.ts +++ b/apps/mobile/src/features/shortcuts/useAppShortcuts.ts @@ -54,7 +54,14 @@ function useShortcutNavigation(): void { } function useRecentThreadShortcutSync(state: NavigationState): void { - const threadRef = useMemo(() => activeThreadRef(state), [state]); + // Launcher shortcuts are Android-only. A null ref on iOS keeps this hook + // (mounted in the root stack layout) from subscribing the root to the + // active thread's shell, which would re-render every screen on each + // title/status/session change. + const threadRef = useMemo( + () => (Platform.OS === "android" ? activeThreadRef(state) : null), + [state], + ); const threadShell = useThreadShell(threadRef); // null until the persisted list loads; recording waits on it so the first // thread opened after a cold start cannot clobber older entries. diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index e328bcef00e..f37b5559a4a 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -946,7 +946,11 @@ export function NewTaskDraftScreen(props: { const promptEditor = ( navigation.goBack() : undefined} - actions={[ - { - accessibilityLabel: "Add project", - icon: "plus", - onPress: () => - navigation.navigate("NewTaskSheet", { - screen: "AddProject", - params: { incomingShareId: routeShareId }, - }), - }, - ]} + actions={ + catalogState.hasReadyEnvironment + ? [ + { + accessibilityLabel: "Add project", + icon: "plus", + onPress: () => + navigation.navigate("NewTaskSheet", { + screen: "AddProject", + params: { incomingShareId: routeShareId }, + }), + }, + ] + : [] + } /> ) : ( @@ -233,16 +237,18 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps ) : null} - - navigation.navigate("NewTaskSheet", { - screen: "AddProject", - params: { incomingShareId: routeShareId }, - }) - } - separateBackground - /> + {catalogState.hasReadyEnvironment ? ( + + navigation.navigate("NewTaskSheet", { + screen: "AddProject", + params: { incomingShareId: routeShareId }, + }) + } + separateBackground + /> + ) : null} )} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 0f1647019c9..efb54ffd06b 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -518,14 +518,16 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const threadKey = scopedThreadKey(props.environmentId, props.selectedThread.id); if (inFlightThreadIdsRef.current.has(threadKey)) return; inFlightThreadIdsRef.current.add(threadKey); - // Sending a prompt starts agent work: arm the lock-screen card now, while - // the app is foregrounded and the activity token can be registered. - armAgentAwarenessLiveActivityForLocalWork({ - threadTitle: props.selectedThread.title, - projectTitle: props.environmentLabel ?? "T3 Code", - }); try { await onSendMessage(); + // Sending a prompt starts agent work: arm the lock-screen card while the + // app is foregrounded and the activity token can be registered. Armed + // after the send so its preference read and native Activity start don't + // contend with the queued-message feedback on the tap frame. + armAgentAwarenessLiveActivityForLocalWork({ + threadTitle: props.selectedThread.title, + projectTitle: props.environmentLabel ?? "T3 Code", + }); } catch { // Send implementations surface user-facing errors; prevent unhandled rejections here. } finally { diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 37a8639fdbd..8ad117c8635 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -48,7 +48,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import Animated, { FadeIn, FadeInUp, - LinearTransition, + useSharedValue, + withTiming, + type LayoutAnimationsValues, type SharedValue, } from "react-native-reanimated"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -80,7 +82,9 @@ import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../. import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography, + scaledTypographyLineHeight, } from "../../lib/appearancePreferences"; +import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; @@ -91,7 +95,12 @@ import { type ThreadFeedLatestTurn, } from "../../lib/threadActivity"; import type { ThreadContentPresentation } from "./threadContentPresentation"; -import { ThreadWorkGroupToggle, ThreadWorkLog } from "./thread-work-log"; +import { + collapsedWorkLogHeight, + ThreadWorkGroupToggle, + ThreadWorkLog, + WORK_GROUP_TOGGLE_HEIGHT, +} from "./thread-work-log"; import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; import { useAssetUrl } from "../../state/assets"; import { resolveWorkspaceRelativeFilePath } from "../files/filePath"; @@ -109,8 +118,22 @@ function formatMessageTime(input: string): string { } // Rows shift when content above them grows (streaming text, work-log folds); -// animating the container position turns those jumps into slides. -const FEED_ITEM_LAYOUT_TRANSITION = LinearTransition.duration(180); +// animating the container position turns those jumps into slides. Applied +// conditionally — see the gated transition in ThreadFeed: while browsing +// history the animation must NOT run, or every estimate→actual size +// correction plays as a visible slide against the instant scroll-offset +// compensation from maintainVisibleContentPosition. +const FEED_ITEM_LAYOUT_DURATION_MS = 180; + +// Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's +// classNames. The fold row's min-h-11 (44px) stays taller than its single +// text-sm line at every supported base font size (26px at the 22pt maximum), +// so its height is a constant; a drifted value costs one correction on +// measure, not a persistent offset. +const TURN_FOLD_HEIGHT = 56; // min-h-11 (44) + mb-3 (12) +// The working row has no min-height clamp — its height follows the scaled +// text-xs line height (see workingRowHeight in ThreadFeed). +const WORKING_ROW_VERTICAL_EXTRAS = 24; // py-1 (8) + mb-4 (16) // Entering animations must only play for rows born just now — LegendList // remounts rows when they scroll back into view, and replaying an entrance for @@ -1301,6 +1324,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const headerMaterialVisibleRef = useRef(false); const previousLatestTurnRef = useRef(props.latestTurn); const { width: windowWidth } = useWindowDimensions(); + const { appearance } = useAppearancePreferences(); const [viewportWidth, setViewportWidth] = useState(() => props.layoutVariant === "split" ? 0 : windowWidth, ); @@ -1410,6 +1434,11 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { }, [props.onHeaderMaterialVisibilityChange], ); + // True while the viewport sits within ~one screen of the list end — the + // only region where layout shifts should animate. Starts true because the + // list opens pinned to the end. + const nearListEnd = useSharedValue(true); + const handleScroll = useCallback( (event: NativeSyntheticEvent) => { // anchorTopInset, not topContentInset: under automatic insets the list @@ -1417,9 +1446,40 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { // UIKit's adjustedContentInset, so topContentInset is 0 here). Add the // header height back or the material toggles a full header too late. reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + anchorTopInset > 6); + const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; + nearListEnd.value = + contentSize.height - layoutMeasurement.height - contentOffset.y < layoutMeasurement.height; }, - [reportHeaderMaterialVisibility, anchorTopInset], + [reportHeaderMaterialVisibility, anchorTopInset, nearListEnd], ); + + // Gated variant of the 180ms feed layout slide. Instant while browsing + // history: maintainVisibleContentPosition compensates the scroll offset in + // the same frame a row's measured size lands, so an instant reposition is + // invisible — animating it is exactly what made cold upward scrolls slide + // and jump. Near the end the slide stays on: streaming growth and sends + // shift rows at rest, where the animation is the thing preventing a hard + // visual snap. + const feedItemLayoutTransition = useMemo(() => { + return (values: LayoutAnimationsValues) => { + "worklet"; + const duration = nearListEnd.value ? FEED_ITEM_LAYOUT_DURATION_MS : 0; + return { + initialValues: { + originX: values.currentOriginX, + originY: values.currentOriginY, + width: values.currentWidth, + height: values.currentHeight, + }, + animations: { + originX: withTiming(values.targetOriginX, { duration }), + originY: withTiming(values.targetOriginY, { duration }), + width: withTiming(values.targetWidth, { duration }), + height: withTiming(values.targetHeight, { duration }), + }, + }; + }; + }, [nearListEnd]); const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { const nextWidth = Math.round(event.nativeEvent.layout.width); const nextHeight = Math.round(event.nativeEvent.layout.height); @@ -1636,6 +1696,38 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { setExpandedImage({ uri, headers }); }, []); + // Rows whose height is known before they ever render. Without this, every + // row above the viewport is assumed to be estimatedItemSize tall, and + // scrolling up through unmeasured content corrects each row's height as it + // mounts — the feed visibly jumps. Fixed sizes make the small chrome rows + // exact; message rows stay undefined and use LegendList's per-type running + // average once one of their type has been measured. Text-driven heights + // follow the configurable base font size via scaledTypographyLineHeight. + const workingRowHeight = + WORKING_ROW_VERTICAL_EXTRAS + + scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.label, appearance.baseFontSize); + const getFixedItemSize = useCallback( + (entry: ThreadFeedEntry) => { + switch (entry.type) { + case "turn-fold": + return TURN_FOLD_HEIGHT; + case "work-toggle": + return WORK_GROUP_TOGGLE_HEIGHT; + case "working": + return workingRowHeight; + case "activity-group": + // Expanded rows append a variable detail block — fall back to + // measurement for those groups. + return entry.activities.some((activity) => expandedWorkRows[activity.id]) + ? undefined + : collapsedWorkLogHeight(entry.activities, appearance.baseFontSize); + default: + return undefined; + } + }, + [expandedWorkRows, workingRowHeight, appearance.baseFontSize], + ); + const renderItem = useCallback( (info: { item: ThreadFeedEntry; index: number }) => renderFeedEntry(info, { @@ -1725,7 +1817,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { } : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} {...(anchoredEndSpace ? { anchoredEndSpace } : {})} - itemLayoutAnimation={FEED_ITEM_LAYOUT_TRANSITION} + itemLayoutAnimation={feedItemLayoutTransition} // Patched LegendList prop (patches/@legendapp__list@3.2.0.patch): // lets its scroll math clamp programmatic scrolls to -headerInset // instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short @@ -1769,6 +1861,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { getItemType={(entry) => entry.type === "message" ? `message:${entry.message.role}` : entry.type } + getFixedItemSize={getFixedItemSize} + // Measure rows well before they scroll into view so estimate→actual + // corrections land offscreen instead of under the user's finger. + drawDistance={500} keyboardShouldPersistTaps="always" keyboardDismissMode="none" keyboardLiftBehavior="whenAtEnd" @@ -1788,6 +1884,11 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { // user overscroll back to the adjusted rest position. scrollToOverflowEnabled estimatedItemSize={180} + // Chat-style bottom alignment: when a thread is shorter than the + // viewport, pad above the content so messages rest just above the + // composer instead of under the header. No effect on threads that + // overflow the viewport (the padding clamps to zero). + alignItemsAtEnd initialScrollAtEnd onScroll={handleScroll} scrollEventThrottle={16} diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index a898ccb9af9..30be4433012 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -26,7 +26,7 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; import { useThreadListV2Enabled } from "./use-thread-list-v2-enabled"; import { environmentServerConfigsAtom } from "../../state/server"; -import { usePendingNewTasks, type PendingNewTask } from "../../state/use-pending-new-tasks"; +import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; import { useWorkspaceState } from "../../state/workspace"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; @@ -62,26 +62,21 @@ import { ThreadListRow, ThreadListShowMoreRow, } from "./thread-list-items"; -import { ThreadListV2Row } from "./thread-list-v2-items"; +import { ThreadListV2PendingRow, ThreadListV2Row } from "./thread-list-v2-items"; import { buildThreadListV2Items, + buildThreadListV2ListItems, THREAD_LIST_V2_SETTLED_INITIAL_COUNT, THREAD_LIST_V2_SETTLED_PAGE_COUNT, - type ThreadListV2Item, + type ThreadListV2ListItem, } from "./threadListV2"; /** The sidebar list serves both lists: v1 grouped items or, when the Thread - List v2 beta is on, queued offline tasks, flat v2 rows, and a settled + List v2 beta is on, flat v2 rows with queued tasks spliced in, and a settled "Show more" pager. */ type SidebarListItem = | HomeListItem - | { - readonly type: "v2-pending-task"; - readonly key: string; - readonly pendingTask: PendingNewTask; - readonly isLast: boolean; - } - | { readonly type: "v2-thread"; readonly key: string; readonly item: ThreadListV2Item } + | ThreadListV2ListItem | { readonly type: "v2-show-more"; readonly key: string; readonly hiddenCount: number }; /** @@ -474,11 +469,11 @@ function ThreadNavigationSidebarPane( }, [nextSnoozeWakeAt, snoozeWakeTick]); const listItems = useMemo(() => { if (!threadListV2Enabled) return listLayout.items; - // Queued offline tasks render above the thread rows (mirrors the - // compact Home v2 list): they are not thread shells, so the v2 item - // builder never sees them, but they must stay visible and deletable - // while their environment is offline. Same environment scope and - // search filter as the list. + // Queued offline tasks are not thread shells, so the v2 item builder + // never sees them; the shared splice puts them below the active block + // (mirrors the compact Home v2 list) where they stay visible and + // deletable while their environment is offline. Same environment scope + // and search filter as the list. const v2SearchQuery = props.searchQuery.trim().toLocaleLowerCase(); const v2PendingTasks = pendingTasks.filter( (pendingTask) => @@ -491,19 +486,10 @@ function ThreadNavigationSidebarPane( (v2SearchQuery.length === 0 || pendingTask.title.toLocaleLowerCase().includes(v2SearchQuery)), ); - const items: SidebarListItem[] = v2PendingTasks.map((pendingTask, index) => ({ - type: "v2-pending-task" as const, - key: `v2-pending:${pendingTask.message.messageId}`, - pendingTask, - isLast: index === v2PendingTasks.length - 1, - })); - for (const item of threadListV2Layout.items) { - items.push({ - type: "v2-thread" as const, - key: scopedThreadKey(item.thread.environmentId, item.thread.id), - item, - }); - } + const items: SidebarListItem[] = buildThreadListV2ListItems({ + items: threadListV2Layout.items, + pendingTasks: v2PendingTasks, + }); if (threadListV2Layout.hiddenSettledCount > 0) { items.push({ type: "v2-show-more", @@ -691,13 +677,26 @@ function ThreadNavigationSidebarPane( onScroll: handleScroll, onScrollBeginDrag: handleScrollBeginDrag, }); + // Project shells load after the first rows draw, so the maps they feed have + // to bust the recycler's memoization — otherwise a row keeps the blank + // favicon and fallback title it was first rendered with. const listExtraData = useMemo( () => ({ selectedThreadKey: props.selectedThreadKey ?? "", + projectByKey, + projectCwdByKey, + projectTitleByProjectKey, savedConnectionsById, serverConfigs, }), - [props.selectedThreadKey, savedConnectionsById, serverConfigs], + [ + props.selectedThreadKey, + projectByKey, + projectCwdByKey, + projectTitleByProjectKey, + savedConnectionsById, + serverConfigs, + ], ); const sidebarItemsAreEqual = useCallback( (previous: SidebarListItem, item: SidebarListItem): boolean => { @@ -712,16 +711,19 @@ function ThreadNavigationSidebarPane( if (previous.type === "v2-show-more" && item.type === "v2-show-more") { return previous.hiddenCount === item.hiddenCount; } - if (previous.type === "v2-pending-task" && item.type === "v2-pending-task") { - return previous.pendingTask === item.pendingTask && previous.isLast === item.isLast; + if (previous.type === "v2-pending" && item.type === "v2-pending") { + return ( + previous.pendingTask === item.pendingTask && + previous.showPendingDivider === item.showPendingDivider + ); } if ( previous.type === "v2-thread" || previous.type === "v2-show-more" || - previous.type === "v2-pending-task" || + previous.type === "v2-pending" || item.type === "v2-thread" || item.type === "v2-show-more" || - item.type === "v2-pending-task" + item.type === "v2-pending" ) { return false; } @@ -749,20 +751,29 @@ function ThreadNavigationSidebarPane( const renderListItem = useCallback( ({ item }: { readonly item: SidebarListItem }) => { switch (item.type) { - case "v2-pending-task": + case "v2-pending": { + const pendingScopeKey = scopedProjectKey( + item.pendingTask.message.environmentId, + item.pendingTask.creation.projectId, + ); return ( - 1 + ? (savedConnectionsById[item.pendingTask.message.environmentId] + ?.environmentLabel ?? null) + : null } - isLast={item.isLast} + pane="sidebar" + showPendingDivider={item.showPendingDivider} onSelectPendingTask={openPendingTask} onDeletePendingTask={confirmDeletePendingTask} /> ); + } case "v2-thread": { const thread = item.item.thread; const scopeKey = scopedProjectKey(thread.environmentId, thread.projectId); diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index 69729cb6469..2ab7e6cf9f4 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -14,6 +14,7 @@ import { ProviderIcon } from "../../components/ProviderIcon"; import { cn } from "../../lib/cn"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; +import type { PendingNewTask } from "../../state/use-pending-new-tasks"; import { useThreadPr } from "../../state/use-thread-pr"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { resolveThreadListV2Status, type ThreadListV2Status } from "./threadListV2"; @@ -68,7 +69,9 @@ const LEGACY_MENU_ACTIONS: MenuAction[] = [ /** Rounded-row radius shared with the v1 sidebar rows. */ const SIDEBAR_V2_ROW_RADIUS = 12; -export const ThreadListV2SettledDivider = memo(function ThreadListV2SettledDivider(props: { +/** Section label + rule: the only structure in an otherwise flat list. */ +export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivider(props: { + readonly label: string; readonly pane?: "screen" | "sidebar"; }) { const borderColor = useThemeColor("--color-border"); @@ -79,12 +82,127 @@ export const ThreadListV2SettledDivider = memo(function ThreadListV2SettledDivid props.pane === "sidebar" ? "px-3" : "px-5", )} > - Settled + {props.label} ); }); +const PENDING_TASK_MENU_ACTIONS: MenuAction[] = [ + { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, +]; + +/** + * A queued new task, in the same idiom as an active v2 row: it is work the + * user wrote, so it reads like the threads it will become. "Queued" takes + * the status slot — the state is the one thing that differs — and stays + * uncolored because nothing is asked of the user; the environment is simply + * not reachable yet. + */ +export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props: { + readonly pendingTask: PendingNewTask; + readonly project: EnvironmentProject | null; + readonly projectTitle?: string; + readonly environmentLabel: string | null; + readonly pane?: "screen" | "sidebar"; + /** Draws the "Pending" divider above the first queued row. */ + readonly showPendingDivider: boolean; + readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void; + readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void; +}) { + const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props; + const drawerColor = useThemeColor("--color-drawer"); + const pressedBackgroundColor = useThemeColor("--color-subtle"); + const sidebarPane = props.pane === "sidebar"; + const projectTitle = + props.projectTitle ?? props.project?.title ?? pendingTask.creation.projectTitle ?? ""; + const branch = pendingTask.creation.branch; + + const handleMenuAction = useCallback( + ({ nativeEvent }: { readonly nativeEvent: { readonly event: string } }) => { + if (nativeEvent.event === "delete") onDeletePendingTask(pendingTask); + }, + [onDeletePendingTask, pendingTask], + ); + + const rowContent = ( + <> + + {props.project ? ( + + ) : null} + + {projectTitle} + + Queued + + {/* One line, unlike the two an active row allows: a queued title is + derived from the whole prompt rather than written as a title, so the + second line is usually a stray word or emoji rather than meaning. */} + + {pendingTask.title} + + {branch || props.environmentLabel ? ( + + {branch ? ( + + {branch} + + ) : null} + {branch && props.environmentLabel ? " · " : null} + {props.environmentLabel ? ( + {props.environmentLabel} + ) : null} + + ) : null} + + ); + + return ( + <> + {props.showPendingDivider ? ( + + ) : null} + + onSelectPendingTask(pendingTask)} + style={ + sidebarPane + ? ({ pressed }) => ({ + backgroundColor: pressed ? pressedBackgroundColor : drawerColor, + borderRadius: SIDEBAR_V2_ROW_RADIUS, + paddingHorizontal: 12, + paddingVertical: 10, + }) + : ({ pressed }) => ({ opacity: pressed ? 0.7 : 1 }) + } + > + {sidebarPane ? ( + rowContent + ) : ( + + {rowContent} + + + )} + + + + ); +}); + export const ThreadListV2Row = memo(function ThreadListV2Row(props: { readonly thread: EnvironmentThreadShell; readonly variant: "card" | "slim"; @@ -423,7 +541,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { return ( <> - {props.showSettledDivider ? : null} + {props.showSettledDivider ? ( + + ) : null} , +): ReadonlyArray { + return activities.filter((activity) => !(activity.toolLike && activity.status === "neutral")); +} + +// Pre-measurement heights for the feed's getFixedItemSize. Collapsed work-log +// rows are single-line (numberOfLines={1}) inside a min-height that stays +// taller than the text at every supported base font size (text-xs reaches +// 23px at the 22pt maximum, under the 32px min-h-8), so row height is +// deterministic. The "work log" label has no such clamp — its height follows +// the scaled text-2xs line height. Values mirror the classNames below — keep +// them in sync; a mismatch only costs a one-time correction on measure. +const WORK_ROW_HEIGHT = 32; // min-h-8 +const WORK_ROW_GAP = 1; // gap-px +const WORK_LOG_HEADER_PADDING = 2; // pb-0.5 under the "work log" label +const WORK_LOG_BOTTOM_MARGIN = 4; // mb-1 + +export const WORK_GROUP_TOGGLE_HEIGHT = 36; // min-h-8 (32) + mb-1 (4) + +export function collapsedWorkLogHeight( + activities: ReadonlyArray, + baseFontSize: number, +): number { + const rows = visibleWorkLogActivities(activities); + if (rows.length === 0) { + return 0; + } + const onlyToolRows = rows.every((row) => row.toolLike); + const headerHeight = + scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.caption, baseFontSize) + WORK_LOG_HEADER_PADDING; + return ( + WORK_LOG_BOTTOM_MARGIN + + (onlyToolRows ? 0 : headerHeight) + + rows.length * WORK_ROW_HEIGHT + + (rows.length - 1) * WORK_ROW_GAP + ); +} + export function ThreadWorkLog(props: { readonly activities: ReadonlyArray; readonly copiedRowId: string | null; @@ -87,9 +129,10 @@ export function ThreadWorkLog(props: { }) { const colorScheme = useColorScheme(); const pressedBackground = colorScheme === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.035)"; - const rows = props.activities - .filter((activity) => !(activity.toolLike && activity.status === "neutral")) - .map((activity) => ({ ...activity, detail: compactActivityDetail(activity.detail) })); + const rows = visibleWorkLogActivities(props.activities).map((activity) => ({ + ...activity, + detail: compactActivityDetail(activity.detail), + })); if (rows.length === 0) { return null; diff --git a/apps/mobile/src/features/threads/threadListV2.test.ts b/apps/mobile/src/features/threads/threadListV2.test.ts index b3e9f73bfe1..1b15905ef7b 100644 --- a/apps/mobile/src/features/threads/threadListV2.test.ts +++ b/apps/mobile/src/features/threads/threadListV2.test.ts @@ -1,9 +1,19 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; -import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId, TurnId } from "@t3tools/contracts"; +import { + CommandId, + EnvironmentId, + MessageId, + ProjectId, + ProviderInstanceId, + ThreadId, + TurnId, +} from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; +import type { PendingNewTask } from "../../state/use-pending-new-tasks"; import { buildThreadListV2Items, + buildThreadListV2ListItems, resolveThreadListV2Enabled, resolveThreadListV2Status, sortThreadsForListV2, @@ -40,43 +50,21 @@ function makeThread( const NOW = "2026-06-02T00:00:00.000Z"; describe("resolveThreadListV2Enabled", () => { - it.each(["development", "preview"])("defaults on for the %s variant", (appVariant) => { - expect( - resolveThreadListV2Enabled({ preference: undefined, preferencesLoaded: true, appVariant }), - ).toBe(true); + it("defaults on when the device has never chosen", () => { + expect(resolveThreadListV2Enabled({ preference: undefined, preferencesLoaded: true })).toBe( + true, + ); }); - it.each(["production", undefined])("defaults off for the %s variant", (appVariant) => { - expect( - resolveThreadListV2Enabled({ preference: undefined, preferencesLoaded: true, appVariant }), - ).toBe(false); + it("honors an explicit device opt-out", () => { + expect(resolveThreadListV2Enabled({ preference: false, preferencesLoaded: true })).toBe(false); + expect(resolveThreadListV2Enabled({ preference: true, preferencesLoaded: true })).toBe(true); }); - it("prefers an explicit device choice over the variant default", () => { - expect( - resolveThreadListV2Enabled({ - preference: false, - preferencesLoaded: true, - appVariant: "preview", - }), - ).toBe(false); - expect( - resolveThreadListV2Enabled({ - preference: true, - preferencesLoaded: true, - appVariant: "production", - }), - ).toBe(true); - }); - - it("holds v1 while preferences are still loading so the list does not remount", () => { - expect( - resolveThreadListV2Enabled({ - preference: undefined, - preferencesLoaded: false, - appVariant: "development", - }), - ).toBe(false); + it("holds the default while preferences are still loading so the list does not remount", () => { + expect(resolveThreadListV2Enabled({ preference: undefined, preferencesLoaded: false })).toBe( + true, + ); }); }); @@ -362,3 +350,88 @@ describe("buildThreadListV2Items settled paging", () => { ]); }); }); + +function makePendingTask(id: string): PendingNewTask { + return { + message: { + environmentId, + threadId: ThreadId.make(`thread-${id}`), + messageId: MessageId.make(id), + commandId: CommandId.make(`command-${id}`), + text: id, + attachments: [], + createdAt: NOW, + creation: { + projectId: ProjectId.make("project-1"), + workspaceMode: "worktree", + branch: null, + worktreePath: null, + }, + }, + creation: { + projectId: ProjectId.make("project-1"), + workspaceMode: "worktree", + branch: null, + worktreePath: null, + }, + title: id, + }; +} + +describe("buildThreadListV2ListItems", () => { + const layout = buildThreadListV2Items({ + threads: [ + makeThread({ id: ThreadId.make("active"), title: "active" }), + makeThread({ + id: ThreadId.make("settled"), + title: "settled", + settledOverride: "settled", + settledAt: NOW, + }), + ], + environmentId: null, + searchQuery: "", + now: NOW, + }); + + it("splices queued tasks between the active block and the settled tail", () => { + const items = buildThreadListV2ListItems({ + items: layout.items, + pendingTasks: [makePendingTask("queued-1"), makePendingTask("queued-2")], + }); + + expect( + items.map((item) => + item.type === "v2-pending" ? item.pendingTask.title : item.item.thread.id, + ), + ).toEqual(["active", "queued-1", "queued-2", "settled"]); + // Only the leading queued row labels the section, exactly like Settled. + expect( + items.filter((item) => item.type === "v2-pending" && item.showPendingDivider), + ).toHaveLength(1); + }); + + it("ends the list with queued tasks when nothing has settled yet", () => { + const activeOnly = buildThreadListV2Items({ + threads: [makeThread({ id: ThreadId.make("active"), title: "active" })], + environmentId: null, + searchQuery: "", + now: NOW, + }); + const items = buildThreadListV2ListItems({ + items: activeOnly.items, + pendingTasks: [makePendingTask("queued-1")], + }); + + expect(items.map((item) => item.type)).toEqual(["v2-thread", "v2-pending"]); + }); + + it("leaves the thread order untouched when nothing is queued", () => { + const items = buildThreadListV2ListItems({ items: layout.items, pendingTasks: [] }); + + expect(items.map((item) => item.key)).toEqual([ + `v2-thread:${environmentId}:active`, + `v2-thread:${environmentId}:settled`, + ]); + }); +}); diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts index 62c0b39aeb8..ab955d16d4d 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -2,6 +2,8 @@ import { effectiveSettled, effectiveSnoozed } from "@t3tools/client-runtime/stat import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId, ProjectId } from "@t3tools/contracts"; +import type { PendingNewTask } from "../../state/use-pending-new-tasks"; + /** * Thread List v2 model, ported from the web sidebar v2 * (apps/web/src/components/Sidebar.logic.ts + SidebarV2.tsx). @@ -19,34 +21,23 @@ export const THREAD_LIST_V2_SETTLED_INITIAL_COUNT = 10; export const THREAD_LIST_V2_SETTLED_PAGE_COUNT = 25; /** - * Whether Thread List v2 is on by default for an app variant. The `development` - * and `preview` variants are mobile's nightly equivalents and opt in; - * `production` stays on v1. Counterpart of web's `resolveSidebarV2Default`. - */ -export function resolveThreadListV2Default(appVariant: unknown): boolean { - return appVariant === "development" || appVariant === "preview"; -} - -/** - * Resolved Thread List v2 state: the device-local preference if the user has - * set one, otherwise the default for this app variant. Preferences persist as - * sparse patches, so `undefined` genuinely means "never chosen". + * Thread List v2 is on by default on every app variant; the Settings → Beta + * toggle is an opt-out. Preferences persist as sparse patches, so `undefined` + * genuinely means "never chosen". * * `preferencesLoaded` guards the startup window: preferences load - * asynchronously, and treating "still loading" as "never chosen" would mount - * v2 on a development build and then flip to v1 once a stored opt-out arrives, - * remounting the whole list. While loading, hold v1 — the state both variants - * already start from. + * asynchronously, and rendering one list before the stored choice arrives would + * remount the whole thing a tick later. While loading, hold the default — that + * is where every device without an explicit opt-out lands anyway. */ export function resolveThreadListV2Enabled(input: { readonly preference: boolean | undefined; readonly preferencesLoaded: boolean; - readonly appVariant: unknown; }): boolean { if (!input.preferencesLoaded) { - return false; + return true; } - return input.preference ?? resolveThreadListV2Default(input.appVariant); + return input.preference ?? true; } export function resolveThreadListV2Status( @@ -124,6 +115,60 @@ export interface ThreadListV2Layout { readonly nextSnoozeWakeAt: string | null; } +export interface ThreadListV2ThreadListItem { + readonly type: "v2-thread"; + readonly key: string; + readonly item: ThreadListV2Item; +} + +export interface ThreadListV2PendingListItem { + readonly type: "v2-pending"; + readonly key: string; + readonly pendingTask: PendingNewTask; + /** First queued row after the active block draws the PENDING divider. */ + readonly showPendingDivider: boolean; +} + +export type ThreadListV2ListItem = ThreadListV2ThreadListItem | ThreadListV2PendingListItem; + +/** + * Splices queued tasks between the active block and the settled tail, so the + * list reads active → pending → settled. Queued work sits below the live + * threads because nothing can happen to it until its environment returns: + * it is waiting, not asking. Shared by the compact Home list and the iPad + * sidebar so both order and label the sections identically. + */ +export function buildThreadListV2ListItems(input: { + readonly items: ReadonlyArray; + readonly pendingTasks: ReadonlyArray; +}): ThreadListV2ListItem[] { + const threadItems = input.items.map( + (item): ThreadListV2ListItem => ({ + type: "v2-thread", + key: `v2-thread:${item.thread.environmentId}:${item.thread.id}`, + item, + }), + ); + if (input.pendingTasks.length === 0) return threadItems; + + const pendingItems = input.pendingTasks.map( + (pendingTask, index): ThreadListV2ListItem => ({ + type: "v2-pending", + key: `v2-pending:${pendingTask.message.messageId}`, + pendingTask, + showPendingDivider: index === 0, + }), + ); + // The settled tail begins at the row that draws the SETTLED divider; with + // no settled rows the queued block simply ends the list. + const settledStart = threadItems.findIndex( + (entry) => entry.type === "v2-thread" && entry.item.showSettledDivider, + ); + return settledStart === -1 + ? [...threadItems, ...pendingItems] + : [...threadItems.slice(0, settledStart), ...pendingItems, ...threadItems.slice(settledStart)]; +} + /** * Partitions visible threads into the active card block (creation order) and * the settled recency tail, matching the web v2 list. `autoSettleAfterDays` diff --git a/apps/mobile/src/features/threads/use-thread-list-v2-enabled.ts b/apps/mobile/src/features/threads/use-thread-list-v2-enabled.ts index bb03b5aa9ad..266bda944ae 100644 --- a/apps/mobile/src/features/threads/use-thread-list-v2-enabled.ts +++ b/apps/mobile/src/features/threads/use-thread-list-v2-enabled.ts @@ -1,18 +1,13 @@ import { useAtomValue } from "@effect/atom-react"; import { AsyncResult } from "effect/unstable/reactivity"; -import Constants from "expo-constants"; import { mobilePreferencesAtom } from "../../state/preferences"; import { resolveThreadListV2Enabled } from "./threadListV2"; /** * Resolved Thread List v2 state: the device-local preference if the user has - * set one, otherwise the default for this app variant (on for development and - * preview, off for production). Every consumer must read through this rather - * than the raw preference, which is undefined until explicitly chosen. - * - * Kept out of `state/preferences.ts` so that module stays importable from node - * test environments, which have no `__DEV__` for expo-constants. + * set one, otherwise the default (on). Every consumer must read through this + * rather than the raw preference, which is undefined until explicitly chosen. */ export function useThreadListV2Enabled(): boolean { const preferencesResult = useAtomValue(mobilePreferencesAtom); @@ -20,6 +15,5 @@ export function useThreadListV2Enabled(): boolean { return resolveThreadListV2Enabled({ preference: loaded ? preferencesResult.value.threadListV2Enabled : undefined, preferencesLoaded: loaded, - appVariant: Constants.expoConfig?.extra?.appVariant, }); } diff --git a/apps/mobile/src/features/updates/app-updates.test.ts b/apps/mobile/src/features/updates/app-updates.test.ts new file mode 100644 index 00000000000..474c99668cd --- /dev/null +++ b/apps/mobile/src/features/updates/app-updates.test.ts @@ -0,0 +1,252 @@ +import { describe, expect, it, vi } from "vite-plus/test"; + +import { + createAppUpdateLaunchCheck, + registerHiddenUpdateTap, + runAppUpdateCheck, + type AppUpdateCheckState, + type AppUpdateClient, +} from "./app-updates"; + +vi.mock("expo-updates", () => ({ + isEnabled: true, + checkForUpdateAsync: vi.fn(), + fetchUpdateAsync: vi.fn(), + reloadAsync: vi.fn(), +})); + +function makeUpdateClient(overrides: Partial = {}): AppUpdateClient { + return { + isEnabled: true, + checkForUpdateAsync: vi.fn(async () => ({ + isAvailable: false, + isRollBackToEmbedded: false, + })), + fetchUpdateAsync: vi.fn(async () => ({ + isNew: true, + isRollBackToEmbedded: false, + })), + reloadAsync: vi.fn(async () => {}), + ...overrides, + }; +} + +describe("runAppUpdateCheck", () => { + it("downloads and restarts when a new update is available", async () => { + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(async () => ({ + isAvailable: true, + isRollBackToEmbedded: false, + })), + }); + const states: AppUpdateCheckState[] = []; + + await runAppUpdateCheck({ client, onStateChange: (state) => states.push(state) }); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(client.fetchUpdateAsync).toHaveBeenCalledOnce(); + expect(client.reloadAsync).toHaveBeenCalledOnce(); + expect(states).toEqual(["checking", "downloading", "restarting"]); + }); + + it("restarts into the embedded bundle for a rollback directive", async () => { + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(async () => ({ + isAvailable: false, + isRollBackToEmbedded: true, + })), + fetchUpdateAsync: vi.fn(async () => ({ + isNew: false, + isRollBackToEmbedded: true, + })), + }); + + await runAppUpdateCheck({ client }); + + expect(client.fetchUpdateAsync).toHaveBeenCalledOnce(); + expect(client.reloadAsync).toHaveBeenCalledOnce(); + }); + + it("stops quietly when the running bundle is current", async () => { + const client = makeUpdateClient(); + const states: AppUpdateCheckState[] = []; + + await runAppUpdateCheck({ client, onStateChange: (state) => states.push(state) }); + + expect(client.fetchUpdateAsync).not.toHaveBeenCalled(); + expect(client.reloadAsync).not.toHaveBeenCalled(); + expect(states).toEqual(["checking", "current"]); + }); + + it("reports manual failures without continuing the update", async () => { + const reportError = vi.spyOn(console, "error").mockImplementation(() => {}); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(async () => { + throw new Error("offline"); + }), + }); + const failures: string[] = []; + const states: AppUpdateCheckState[] = []; + + await runAppUpdateCheck({ + client, + onFailure: (message) => failures.push(message), + onStateChange: (state) => states.push(state), + }); + + expect(client.fetchUpdateAsync).not.toHaveBeenCalled(); + expect(failures).toEqual(["offline"]); + expect(states).toEqual(["checking", "idle"]); + reportError.mockRestore(); + }); + + it("coalesces overlapping launch and manual checks", async () => { + let resolveCheck!: (result: { + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }) => void; + const checkResult = new Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>((resolve) => { + resolveCheck = resolve; + }); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(() => checkResult), + }); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + const manualStates: AppUpdateCheckState[] = []; + + const launchCheck = checkOnLaunch(); + const manualCheck = runAppUpdateCheck({ + client, + onStateChange: (state) => manualStates.push(state), + }); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(manualStates).toEqual(["checking"]); + + resolveCheck({ + isAvailable: false, + isRollBackToEmbedded: false, + }); + await Promise.all([launchCheck, manualCheck]); + + expect(manualStates).toEqual(["checking", "current"]); + + await runAppUpdateCheck({ client }); + expect(client.checkForUpdateAsync).toHaveBeenCalledTimes(2); + }); + + it("forwards failures to a manual check coalesced with the launch check", async () => { + const reportError = vi.spyOn(console, "error").mockImplementation(() => {}); + let rejectCheck!: (error: Error) => void; + const checkResult = new Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>((_resolve, reject) => { + rejectCheck = reject; + }); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(() => checkResult), + }); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + const failures: string[] = []; + const manualStates: AppUpdateCheckState[] = []; + + const launchCheck = checkOnLaunch(); + const manualCheck = runAppUpdateCheck({ + client, + onFailure: (message) => failures.push(message), + onStateChange: (state) => manualStates.push(state), + }); + + rejectCheck(new Error("offline")); + await Promise.all([launchCheck, manualCheck]); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(failures).toEqual(["offline"]); + expect(manualStates).toEqual(["checking", "idle"]); + reportError.mockRestore(); + }); + + it("publishes the in-flight check before a state callback can re-enter", async () => { + let resolveCheck!: (result: { + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }) => void; + const checkResult = new Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>((resolve) => { + resolveCheck = resolve; + }); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(() => checkResult), + }); + const reentrantStates: AppUpdateCheckState[] = []; + let reentrantCheck: Promise | undefined; + let didReenter = false; + + const initialCheck = runAppUpdateCheck({ + client, + onStateChange: (state) => { + if (state !== "checking" || didReenter) return; + didReenter = true; + reentrantCheck = runAppUpdateCheck({ + client, + onStateChange: (reentrantState) => reentrantStates.push(reentrantState), + }); + }, + }); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(reentrantCheck).toBeDefined(); + expect(reentrantStates).toEqual(["checking"]); + + resolveCheck({ + isAvailable: false, + isRollBackToEmbedded: false, + }); + await Promise.all([initialCheck, reentrantCheck]); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(reentrantStates).toEqual(["checking", "current"]); + }); +}); + +describe("createAppUpdateLaunchCheck", () => { + it("checks at most once for each JavaScript launch", async () => { + const client = makeUpdateClient(); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + + const first = checkOnLaunch(); + const second = checkOnLaunch(); + await first; + + expect(second).toBeUndefined(); + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + }); + + it("does nothing when Expo updates are disabled", () => { + const client = makeUpdateClient({ isEnabled: false }); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + + expect(checkOnLaunch()).toBeUndefined(); + expect(client.checkForUpdateAsync).not.toHaveBeenCalled(); + }); +}); + +describe("registerHiddenUpdateTap", () => { + it("unlocks the manual check on the fifth tap", () => { + let count = 0; + + for (let tap = 1; tap <= 5; tap += 1) { + const result = registerHiddenUpdateTap(count); + expect(result.shouldCheck).toBe(tap === 5); + count = result.nextCount; + } + + expect(count).toBe(0); + }); +}); diff --git a/apps/mobile/src/features/updates/app-updates.ts b/apps/mobile/src/features/updates/app-updates.ts new file mode 100644 index 00000000000..ab896b53c07 --- /dev/null +++ b/apps/mobile/src/features/updates/app-updates.ts @@ -0,0 +1,228 @@ +import * as Updates from "expo-updates"; + +import { + type AtomCommandResult, + isAtomCommandInterrupted, + reportAtomCommandResult, + settlePromise, + squashAtomCommandFailure, +} from "@t3tools/client-runtime/state/runtime"; + +export type AppUpdateCheckState = "idle" | "checking" | "downloading" | "restarting" | "current"; + +export interface AppUpdateClient { + readonly isEnabled: boolean; + readonly checkForUpdateAsync: () => Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>; + readonly fetchUpdateAsync: () => Promise<{ + readonly isNew: boolean; + readonly isRollBackToEmbedded: boolean; + }>; + readonly reloadAsync: () => Promise; +} + +interface AppUpdateCheckOptions { + readonly client?: AppUpdateClient; + readonly onFailure?: (message: string) => void; + readonly onStateChange?: (state: AppUpdateCheckState) => void; +} + +interface AppUpdateCheckProgress { + failure: string | undefined; + state: AppUpdateCheckState | undefined; +} + +interface AppUpdateCheckInFlight { + readonly failureListeners: Set>; + readonly progress: AppUpdateCheckProgress; + readonly promise: Promise; + readonly stateListeners: Set>; +} + +interface Deferred { + readonly promise: Promise; + readonly reject: (cause: unknown) => void; + readonly resolve: () => void; +} + +const HIDDEN_UPDATE_TAP_COUNT = 5; +let appUpdateCheckInFlight: AppUpdateCheckInFlight | undefined; + +/** + * Keeps the manual update affordance discoverable only to someone deliberately + * tapping the version row five times. + */ +export function registerHiddenUpdateTap(count: number): { + readonly nextCount: number; + readonly shouldCheck: boolean; +} { + const nextCount = count + 1; + if (nextCount >= HIDDEN_UPDATE_TAP_COUNT) { + return { + nextCount: 0, + shouldCheck: true, + }; + } + return { + nextCount, + shouldCheck: false, + }; +} + +export async function runAppUpdateCheck(options: AppUpdateCheckOptions = {}): Promise { + const client = options.client ?? Updates; + if (!client.isEnabled) return; + + if (appUpdateCheckInFlight) { + await observeAppUpdateCheck(appUpdateCheckInFlight, options); + return; + } + + const progress: AppUpdateCheckProgress = { + failure: undefined, + state: undefined, + }; + const failureListeners = new Set>(); + const stateListeners = new Set>(); + if (options.onFailure) failureListeners.add(options.onFailure); + if (options.onStateChange) stateListeners.add(options.onStateChange); + + const deferred = createDeferred(); + const inFlight: AppUpdateCheckInFlight = { + failureListeners, + progress, + promise: deferred.promise, + stateListeners, + }; + // Publish the operation before any state listener can synchronously re-enter. + appUpdateCheckInFlight = inFlight; + + const execution = performAppUpdateCheck(client, { + onFailure: (message) => { + progress.failure = message; + notifyListeners(failureListeners, message); + }, + onStateChange: (state) => { + progress.state = state; + notifyListeners(stateListeners, state); + }, + }); + void execution.then(deferred.resolve, deferred.reject); + + try { + await deferred.promise; + } finally { + if (appUpdateCheckInFlight === inFlight) { + appUpdateCheckInFlight = undefined; + } + } +} + +function createDeferred(): Deferred { + let reject!: Deferred["reject"]; + let resolve!: Deferred["resolve"]; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = () => resolvePromise(); + reject = rejectPromise; + }); + return { promise, reject, resolve }; +} + +function notifyListeners(listeners: ReadonlySet<(value: A) => void>, value: A): void { + // A listener can synchronously subscribe another caller. Snapshot so that + // caller receives only observeAppUpdateCheck's explicit current-value replay. + const snapshot = Array.from(listeners); + for (const listener of snapshot) listener(value); +} + +async function observeAppUpdateCheck( + inFlight: AppUpdateCheckInFlight, + options: AppUpdateCheckOptions, +): Promise { + const onFailure = options.onFailure; + const onStateChange = options.onStateChange; + + if (onFailure) { + inFlight.failureListeners.add(onFailure); + if (inFlight.progress.failure) onFailure(inFlight.progress.failure); + } + if (onStateChange) { + inFlight.stateListeners.add(onStateChange); + if (inFlight.progress.state) onStateChange(inFlight.progress.state); + } + + try { + await inFlight.promise; + } finally { + if (onFailure) inFlight.failureListeners.delete(onFailure); + if (onStateChange) inFlight.stateListeners.delete(onStateChange); + } +} + +async function performAppUpdateCheck( + client: AppUpdateClient, + options: AppUpdateCheckOptions, +): Promise { + const setState = options.onStateChange ?? (() => {}); + + setState("checking"); + const check = await settlePromise(() => client.checkForUpdateAsync()); + if (check._tag === "Failure") { + reportUpdateFailure(check, "Could not check for updates.", options.onFailure); + setState("idle"); + return; + } + // A rollback directive (`eas update:rollback`) arrives as isAvailable: false + // with isRollBackToEmbedded: true. The running OTA still has to be dropped. + if (!check.value.isAvailable && !check.value.isRollBackToEmbedded) { + setState("current"); + return; + } + + setState("downloading"); + const fetched = await settlePromise(() => client.fetchUpdateAsync()); + if (fetched._tag === "Failure") { + reportUpdateFailure(fetched, "Could not download the update.", options.onFailure); + setState("idle"); + return; + } + // isNew is always false for a rollback, so it cannot be the sole gate. + if (!fetched.value.isNew && !fetched.value.isRollBackToEmbedded) { + setState("current"); + return; + } + + setState("restarting"); + const reloaded = await settlePromise(() => client.reloadAsync()); + if (reloaded._tag === "Failure") { + reportUpdateFailure(reloaded, "Downloaded, but could not restart the app.", options.onFailure); + setState("idle"); + } +} + +function reportUpdateFailure( + result: AtomCommandResult, + fallback: string, + onFailure: AppUpdateCheckOptions["onFailure"], +): void { + reportAtomCommandResult(result, { label: "app update check" }); + if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; + const error = squashAtomCommandFailure(result); + onFailure?.(error instanceof Error ? error.message : fallback); +} + +export function createAppUpdateLaunchCheck( + client: AppUpdateClient = Updates, +): () => Promise | undefined { + let started = false; + + return () => { + if (started || !client.isEnabled) return undefined; + started = true; + return runAppUpdateCheck({ client }); + }; +} + +export const checkForAppUpdateOnLaunch = createAppUpdateLaunchCheck(); diff --git a/apps/mobile/src/lib/appearancePreferences.ts b/apps/mobile/src/lib/appearancePreferences.ts index 7287eabba7c..d2504a629dd 100644 --- a/apps/mobile/src/lib/appearancePreferences.ts +++ b/apps/mobile/src/lib/appearancePreferences.ts @@ -198,12 +198,27 @@ export function resolveTextScaleVariables(baseFontSize: number): Record): ThreadFeedEntry[] { const grouped: ThreadFeedEntry[] = []; + // Mutable backing array for the trailing group so appending an activity is + // O(1) instead of re-copying the group (which made this loop quadratic on + // long tool runs). The array is only mutated while it is the trailing group. + let openGroupActivities: ThreadFeedActivity[] | null = null; + let openGroupTurnId: TurnId | null = null; for (const entry of entries) { // Skip empty messages so they don't break activity grouping. @@ -966,24 +971,23 @@ function groupAdjacentActivities(entries: ReadonlyArray): Th if (entry.type !== "activity") { grouped.push(entry); + openGroupActivities = null; continue; } - const previous = grouped.at(-1); - if (previous?.type === "activity-group" && previous.turnId === entry.turnId) { - grouped[grouped.length - 1] = { - ...previous, - activities: [...previous.activities, entry.activity], - }; + if (openGroupActivities !== null && openGroupTurnId === entry.turnId) { + openGroupActivities.push(entry.activity); continue; } + openGroupActivities = [entry.activity]; + openGroupTurnId = entry.turnId; grouped.push({ type: "activity-group", id: entry.id, createdAt: entry.createdAt, turnId: entry.turnId, - activities: [entry.activity], + activities: openGroupActivities, }); } @@ -1225,13 +1229,24 @@ function appendPresentedFeedEntry( }); } -export function derivePendingApprovals( +/** + * Sorts activities into lifecycle order. `derivePendingApprovals` and + * `derivePendingUserInputs` both expect this ordering; sorting once and + * passing the result to both avoids re-sorting the full activity history + * per derivation. + */ +export function sortThreadActivities( activities: ReadonlyArray, +): ReadonlyArray { + return Arr.sort(activities, activityOrder); +} + +export function derivePendingApprovals( + sortedActivities: ReadonlyArray, ): PendingApproval[] { const openByRequestId = new Map(); - const ordered = Arr.sort(activities, activityOrder); - for (const activity of ordered) { + for (const activity of sortedActivities) { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record) @@ -1273,12 +1288,11 @@ export function derivePendingApprovals( } export function derivePendingUserInputs( - activities: ReadonlyArray, + sortedActivities: ReadonlyArray, ): PendingUserInput[] { const openByRequestId = new Map(); - const ordered = Arr.sort(activities, activityOrder); - for (const activity of ordered) { + for (const activity of sortedActivities) { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record) diff --git a/apps/mobile/src/native/StackHeader.tsx b/apps/mobile/src/native/StackHeader.tsx index 78c87119512..ddfbc64d6c1 100644 --- a/apps/mobile/src/native/StackHeader.tsx +++ b/apps/mobile/src/native/StackHeader.tsx @@ -340,7 +340,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { return { type: "spacing", spacing: typeof child.props.width === "number" ? child.props.width : 8, - }; + flexible: Boolean(child.props.flexible), + } as NativeStackHeaderItem; } return null; @@ -351,6 +352,11 @@ function collectToolbarItems(children: ReactNode): NativeStackHeaderItem[] { Children.forEach(children, (child) => { const item = convertToolbarChild(child); if (item) { + if (item.type === "spacing") { + // Native inserts spacing items at `index`, treating a missing index + // as 0 — which would move the spacer in front of earlier siblings. + (item as { index?: number }).index = items.length; + } items.push(item); } }); @@ -440,6 +446,7 @@ function NativeHeaderToolbarLabel(_props: { readonly children?: ReactNode }) { NativeHeaderToolbarLabel.displayName = "NativeHeaderToolbarLabel"; function NativeHeaderToolbarSpacer(_props: { + readonly flexible?: boolean; readonly sharesBackground?: boolean; readonly width?: number; }) { diff --git a/apps/mobile/src/persistence/mobile-preferences.ts b/apps/mobile/src/persistence/mobile-preferences.ts index bbcf4131f31..6b1018e2a0a 100644 --- a/apps/mobile/src/persistence/mobile-preferences.ts +++ b/apps/mobile/src/persistence/mobile-preferences.ts @@ -25,9 +25,9 @@ export interface Preferences { readonly projectGroupingEnabled?: boolean; /** * Device-local mirror of the web beta's `sidebarV2Enabled`. Mobile has no - * client-settings sync, so the flat v2 thread list is opted into per - * device. Undefined means the user has never chosen, in which case the app - * variant decides — see `resolveThreadListV2Enabled`. + * client-settings sync, so the flat v2 thread list is opted out of per + * device. Undefined means the user has never chosen, which resolves to on — + * see `resolveThreadListV2Enabled`. */ readonly threadListV2Enabled?: boolean; } diff --git a/apps/mobile/src/state/thread-outbox-manager.ts b/apps/mobile/src/state/thread-outbox-manager.ts index 19f89d13c51..f6a20ccffc2 100644 --- a/apps/mobile/src/state/thread-outbox-manager.ts +++ b/apps/mobile/src/state/thread-outbox-manager.ts @@ -88,11 +88,23 @@ export function createThreadOutboxManager(options: ThreadOutboxManagerOptions) { return loadPromise; }; - const enqueue = (message: QueuedThreadMessage): Promise => - serialize(async () => { + // The queued atom drives the composer's immediate "queued" feedback, so it + // is published synchronously; the durable write happens behind it and rolls + // the message back out if it fails (durability only matters for crash + // recovery, not for the in-session queue). + const enqueue = (message: QueuedThreadMessage): Promise => { + setMessages([ + ...currentMessages().filter((candidate) => candidate.messageId !== message.messageId), + message, + ]); + return serialize(async () => { try { await options.storage.write(message); } catch (cause) { + // Roll back by reference, not messageId: a retry enqueue with the same + // id may have optimistically replaced this attempt while the write was + // in flight, and its entry must survive this attempt's failure. + setMessages(currentMessages().filter((candidate) => candidate !== message)); throw new ThreadOutboxManagerError({ operation: "enqueue", environmentId: message.environmentId, @@ -101,11 +113,15 @@ export function createThreadOutboxManager(options: ThreadOutboxManagerOptions) { cause, }); } - setMessages([ - ...currentMessages().filter((candidate) => candidate.messageId !== message.messageId), - message, - ]); }); + }; + + // Resolves once all pending mutations (including any in-flight enqueue + // write) have settled, reporting whether the message is still queued. The + // drain awaits this before dispatching so a message whose durable write + // later fails can never have been delivered first. + const confirmQueued = (message: QueuedThreadMessage): Promise => + serialize(async () => currentMessages().some((candidate) => candidate === message)); // Rewrites an already-queued message. A no-op when the message has been // removed in the meantime (e.g. deleted or delivered), so a trailing editor @@ -204,6 +220,7 @@ export function createThreadOutboxManager(options: ThreadOutboxManagerOptions) { serialize, load, enqueue, + confirmQueued, update, remove, clearEnvironment, diff --git a/apps/mobile/src/state/thread-outbox.test.ts b/apps/mobile/src/state/thread-outbox.test.ts index 6c665c432f4..89f8b26798b 100644 --- a/apps/mobile/src/state/thread-outbox.test.ts +++ b/apps/mobile/src/state/thread-outbox.test.ts @@ -294,6 +294,111 @@ describe("thread outbox", () => { registry.dispose(); }); + it("publishes an enqueued message before the durable write resolves", async () => { + const registry = AtomRegistry.make(); + let releaseWrite!: () => void; + const writeBlocked = new Promise((resolve) => { + releaseWrite = resolve; + }); + const manager = createThreadOutboxManager({ + registry, + storage: { + load: async () => [], + write: async () => writeBlocked, + remove: async () => undefined, + }, + }); + const message = queuedMessage({ + messageId: "message-1", + createdAt: "2026-06-08T10:00:01.000Z", + }); + + const enqueueing = manager.enqueue(message); + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({ + "environment-1:thread-1": [message], + }); + + releaseWrite(); + await enqueueing; + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({ + "environment-1:thread-1": [message], + }); + registry.dispose(); + }); + + it("rolls an enqueued message back out when the durable write fails", async () => { + const registry = AtomRegistry.make(); + const writeCause = new Error("disk full"); + const manager = createThreadOutboxManager({ + registry, + storage: { + load: async () => [], + write: async () => { + throw writeCause; + }, + remove: async () => undefined, + }, + }); + const message = queuedMessage({ + messageId: "message-1", + createdAt: "2026-06-08T10:00:01.000Z", + }); + + await expect(manager.enqueue(message)).rejects.toEqual( + new ThreadOutboxManagerError({ + operation: "enqueue", + environmentId: message.environmentId, + threadId: message.threadId, + messageId: message.messageId, + cause: writeCause, + }), + ); + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({}); + registry.dispose(); + }); + + it("keeps a same-id retry queued when the first attempt's write fails", async () => { + const registry = AtomRegistry.make(); + let failNextWrite = true; + let releaseFirstWrite!: () => void; + const firstWriteBlocked = new Promise((resolve) => { + releaseFirstWrite = resolve; + }); + const manager = createThreadOutboxManager({ + registry, + storage: { + load: async () => [], + write: async () => { + if (failNextWrite) { + failNextWrite = false; + await firstWriteBlocked; + throw new Error("disk full"); + } + }, + remove: async () => undefined, + }, + }); + const message = queuedMessage({ + messageId: "message-1", + createdAt: "2026-06-08T10:00:01.000Z", + }); + const retried = { ...message, text: "retried" }; + + const first = manager.enqueue(message); + const second = manager.enqueue(retried); + releaseFirstWrite(); + await expect(first).rejects.toBeInstanceOf(ThreadOutboxManagerError); + await second; + + // The failed first attempt must not roll back the retry that replaced it. + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({ + "environment-1:thread-1": [retried], + }); + await expect(manager.confirmQueued(retried)).resolves.toBe(true); + await expect(manager.confirmQueued(message)).resolves.toBe(false); + registry.dispose(); + }); + it("replaces an existing message when an enqueue retry uses the same id", async () => { const registry = AtomRegistry.make(); const manager = createThreadOutboxManager({ diff --git a/apps/mobile/src/state/thread-outbox.ts b/apps/mobile/src/state/thread-outbox.ts index a089c49732c..59287b12e61 100644 --- a/apps/mobile/src/state/thread-outbox.ts +++ b/apps/mobile/src/state/thread-outbox.ts @@ -20,6 +20,11 @@ export function enqueueThreadOutboxMessage(message: QueuedThreadMessage): Promis return threadOutboxManager.enqueue(message); } +/** Waits for pending writes to settle; false if the message was rolled back. */ +export function confirmThreadOutboxMessageQueued(message: QueuedThreadMessage): Promise { + return threadOutboxManager.confirmQueued(message); +} + /** Rewrite a queued message; no-op (false) if it was removed in the meantime. */ export function updateThreadOutboxMessage(message: QueuedThreadMessage): Promise { return threadOutboxManager.update(message); diff --git a/apps/mobile/src/state/use-selected-thread-requests.ts b/apps/mobile/src/state/use-selected-thread-requests.ts index c9e9db12530..82ff42f247a 100644 --- a/apps/mobile/src/state/use-selected-thread-requests.ts +++ b/apps/mobile/src/state/use-selected-thread-requests.ts @@ -11,6 +11,7 @@ import { derivePendingApprovals, derivePendingUserInputs, setPendingUserInputCustomAnswer, + sortThreadActivities, type PendingUserInputDraftAnswer, } from "../lib/threadActivity"; import { appAtomRegistry } from "./atom-registry"; @@ -70,14 +71,19 @@ export function useSelectedThreadRequests() { null, ); - const activePendingApprovals = useMemo( - () => (selectedThread ? derivePendingApprovals(selectedThread.activities) : []), + // Sort once; both derivations expect the same lifecycle ordering. + const sortedActivities = useMemo( + () => (selectedThread ? sortThreadActivities(selectedThread.activities) : []), [selectedThread], ); + const activePendingApprovals = useMemo( + () => derivePendingApprovals(sortedActivities), + [sortedActivities], + ); const activePendingApproval = activePendingApprovals[0] ?? null; const activePendingUserInputs = useMemo( - () => (selectedThread ? derivePendingUserInputs(selectedThread.activities) : []), - [selectedThread], + () => derivePendingUserInputs(sortedActivities), + [sortedActivities], ); const activePendingUserInput = activePendingUserInputs[0] ?? null; const activePendingUserInputDrafts = diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts index 90831f8437a..b09aadf7e6b 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts @@ -30,6 +30,7 @@ import { composerDraftsAtom, ensureComposerDraftsLoaded, getComposerDraftSnapshot, + mergeComposerDraftContent, removeComposerDraftAttachment, setComposerDraftText, updateComposerDraftSettings, @@ -148,27 +149,36 @@ export function useThreadComposerState() { const metadata = makeQueuedMessageMetadata(); const messageId = MessageId.make(metadata.messageId); - try { - await enqueueThreadOutboxMessage({ - environmentId: selectedThreadShell.environmentId, - threadId: selectedThreadShell.id, - messageId, - commandId: CommandId.make(metadata.commandId), - text, - attachments, - modelSelection: draft.modelSelection ?? thread.modelSelection, - runtimeMode: draft.runtimeMode ?? thread.runtimeMode, - interactionMode: draft.interactionMode ?? thread.interactionMode, - createdAt: metadata.createdAt, - }); - clearComposerDraftContent(threadKey); - return messageId; - } catch (error) { + // Enqueue publishes the queued atom synchronously (the durable write + // happens behind it), so clearing the draft here gives send feedback on + // the tap frame instead of after file I/O. If the write fails the message + // is rolled out of the queue and the content is merged back into the + // draft, preserving anything typed since. + const enqueuePromise = enqueueThreadOutboxMessage({ + environmentId: selectedThreadShell.environmentId, + threadId: selectedThreadShell.id, + messageId, + commandId: CommandId.make(metadata.commandId), + text, + attachments, + modelSelection: draft.modelSelection ?? thread.modelSelection, + runtimeMode: draft.runtimeMode ?? thread.runtimeMode, + interactionMode: draft.interactionMode ?? thread.interactionMode, + createdAt: metadata.createdAt, + }); + clearComposerDraftContent(threadKey); + enqueuePromise.catch((error: unknown) => { + // Restore text via merge (idempotent) but attachments via the uncapped + // append: the merge path slots existing attachments first and truncates + // at the send limit, which would silently drop this message's images if + // the user attached new ones while the write was in flight. + void mergeComposerDraftContent(threadKey, { text, attachments: [] }); + appendComposerDraftAttachments(threadKey, attachments); setPendingConnectionError( error instanceof Error ? error.message : "Failed to save the queued message.", ); - return null; - } + }); + return messageId; }, [selectedThreadDetail, selectedThreadShell]); const onChangeDraftMessage = useCallback( diff --git a/apps/mobile/src/state/use-thread-outbox-drain.ts b/apps/mobile/src/state/use-thread-outbox-drain.ts index 3559fa140fe..d06a4098aab 100644 --- a/apps/mobile/src/state/use-thread-outbox-drain.ts +++ b/apps/mobile/src/state/use-thread-outbox-drain.ts @@ -21,7 +21,11 @@ import { toUploadChatImageAttachments } from "../lib/composerImages"; import { randomHex } from "../lib/uuid"; import { appAtomRegistry } from "./atom-registry"; import { useProjects, useThreadShells } from "./entities"; -import { ensureThreadOutboxLoaded, removeThreadOutboxMessage } from "./thread-outbox"; +import { + confirmThreadOutboxMessageQueued, + ensureThreadOutboxLoaded, + removeThreadOutboxMessage, +} from "./thread-outbox"; import { isQueuedThreadCreationSendable, modelSelectionsEqual, @@ -33,7 +37,7 @@ import { type QueuedThreadMessage, type ThreadOutboxCommandStage, } from "./thread-outbox-model"; -import { threadEnvironment } from "./threads"; +import { environmentThreadShells, threadEnvironment } from "./threads"; import { useAtomCommand } from "./use-atom-command"; import { editingQueuedMessageIdsAtom, @@ -349,8 +353,32 @@ export function useThreadOutboxDrain(): void { return false; }, ); - const delivery = - deliveryAction === "remove" + // Enqueues publish optimistically before their durable write settles. + // Confirm the write landed (and the message wasn't rolled back) before + // sending, so a failed write can never chase an already-delivered turn. + const delivery = confirmThreadOutboxMessageQueued(nextQueuedMessage).then((queued) => { + if (!queued) { + // Rolled back by a failed write; nothing to deliver or retry. + return true; + } + // The guards evaluated before the confirmation await are stale by now: + // the thread may have gone busy, or the user may have opened this + // message in the editor. Re-read both and defer to the next drain pass + // (returning true skips the failure/backoff path) rather than sending + // a payload the user is editing or racing an active turn. + if (appAtomRegistry.get(editingQueuedMessageIdsAtom)[nextQueuedMessage.messageId]) { + return true; + } + const freshThread = findThread( + appAtomRegistry.get(environmentThreadShells.threadShellsAtom), + nextQueuedMessage, + ); + const freshThreadBusy = + freshThread?.session?.status === "running" || freshThread?.session?.status === "starting"; + if (deliveryAction === "send" && creation === undefined && freshThreadBusy) { + return true; + } + return deliveryAction === "remove" ? removeQueuedMessage("[thread-outbox] failed to remove message for a missing thread") : creation !== undefined ? creationProjectCwd !== null @@ -359,6 +387,7 @@ export function useThreadOutboxDrain(): void { : thread !== undefined ? sendQueuedMessage(nextQueuedMessage, thread) : Promise.resolve(false); + }); void delivery .then((sent) => { if (sent) { diff --git a/apps/web/src/components/CommandPalette.logic.test.ts b/apps/web/src/components/CommandPalette.logic.test.ts index 3d03c877c25..04de1784715 100644 --- a/apps/web/src/components/CommandPalette.logic.test.ts +++ b/apps/web/src/components/CommandPalette.logic.test.ts @@ -4,8 +4,6 @@ import type { Thread } from "../types"; import { buildBrowseGroups, buildThreadActionItems, - canPreloadBrowsePath, - createBrowseNavigationCoordinator, enumerateCommandPaletteItems, filterCommandPaletteGroups, type CommandPaletteGroup, @@ -234,65 +232,3 @@ describe("buildBrowseGroups", () => { expect(actionSettled).toBe(true); }); }); - -describe("createBrowseNavigationCoordinator", () => { - it("only commits the latest overlapping navigation", async () => { - const coordinator = createBrowseNavigationCoordinator(); - let finishFirst: (() => void) | undefined; - let finishSecond: (() => void) | undefined; - const commits: string[] = []; - - const first = coordinator.run({ - load: () => - new Promise((resolve) => { - finishFirst = resolve; - }), - commit: () => { - commits.push("first"); - }, - }); - const second = coordinator.run({ - load: () => - new Promise((resolve) => { - finishSecond = resolve; - }), - commit: () => { - commits.push("second"); - }, - }); - - finishSecond?.(); - await expect(second).resolves.toBe(true); - finishFirst?.(); - await expect(first).resolves.toBe(false); - expect(commits).toEqual(["second"]); - }); - - it("does not commit after newer user input invalidates the navigation", async () => { - const coordinator = createBrowseNavigationCoordinator(); - let finishNavigation: (() => void) | undefined; - const commit = vi.fn(); - const navigation = coordinator.run({ - load: () => - new Promise((resolve) => { - finishNavigation = resolve; - }), - commit, - }); - - coordinator.invalidate(); - finishNavigation?.(); - - await expect(navigation).resolves.toBe(false); - expect(commit).not.toHaveBeenCalled(); - }); -}); - -describe("canPreloadBrowsePath", () => { - it("only preloads paths for connected environments", () => { - expect(canPreloadBrowsePath("connected")).toBe(true); - expect(canPreloadBrowsePath("offline")).toBe(false); - expect(canPreloadBrowsePath("reconnecting")).toBe(false); - expect(canPreloadBrowsePath(null)).toBe(false); - }); -}); diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index f6f5a08352c..058322744bb 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -1,9 +1,8 @@ import { - type KeybindingCommand, type FilesystemBrowseEntry, + type KeybindingCommand, THREAD_JUMP_KEYBINDING_COMMANDS, } from "@t3tools/contracts"; -import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings"; import * as Arr from "effect/Array"; import * as Result from "effect/Result"; @@ -16,39 +15,6 @@ export const RECENT_THREAD_LIMIT = 12; export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80"; export const ADDON_ICON_CLASS = "size-4"; -export interface BrowseNavigationCoordinator { - readonly invalidate: () => void; - readonly run: (input: { - readonly load: () => Promise; - readonly commit: () => void; - }) => Promise; -} - -export function createBrowseNavigationCoordinator(): BrowseNavigationCoordinator { - let generation = 0; - - return { - invalidate: () => { - generation += 1; - }, - run: async (input) => { - const navigationGeneration = ++generation; - await input.load(); - if (navigationGeneration !== generation) { - return false; - } - input.commit(); - return true; - }, - }; -} - -export function canPreloadBrowsePath( - connectionPhase: EnvironmentConnectionPhase | null | undefined, -): boolean { - return connectionPhase === "connected"; -} - export interface CommandPaletteItem { readonly kind: "action" | "submenu"; readonly value: string; @@ -104,30 +70,6 @@ export function enumerateCommandPaletteItems( export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-browse"; -export function filterBrowseEntries(input: { - browseEntries: ReadonlyArray; - browseFilterQuery: string; -}): { - filteredEntries: FilesystemBrowseEntry[]; - exactEntry: FilesystemBrowseEntry | null; -} { - const lowerFilter = input.browseFilterQuery.toLowerCase(); - const showHidden = input.browseFilterQuery.startsWith("."); - - const filteredEntries = input.browseEntries.filter( - (entry) => - entry.name.toLowerCase().startsWith(lowerFilter) && - (showHidden || !entry.name.startsWith(".")), - ); - - const exactEntry = - input.browseFilterQuery.length > 0 - ? (filteredEntries.find((entry) => entry.name === input.browseFilterQuery) ?? null) - : null; - - return { filteredEntries, exactEntry }; -} - export function normalizeSearchText(value: string): string { return value.trim().toLowerCase().replace(/\s+/g, " "); } diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 9b6bfc0545b..072929a3447 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -1,6 +1,14 @@ "use client"; import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects"; +import { connectionStatusText } from "@t3tools/client-runtime/connection"; +import { + canPreloadBrowsePath, + createBrowseNavigationCoordinator, + filterFilesystemBrowseEntries, + getFilesystemBrowsePath, +} from "@t3tools/client-runtime/state/filesystem"; import { isAtomCommandInterrupted, settlePromise, @@ -61,16 +69,12 @@ import { useProjects, useThreadShells } from "../state/entities"; import { resolveThreadActionProjectRef, startNewThreadFromContext } from "../lib/chatThreadActions"; import { appendBrowsePathSegment, - canNavigateUp, ensureBrowseDirectoryPath, findProjectByPath, getBrowseDirectoryPath, - getBrowseLeafPathSegment, - getBrowseParentPath, hasTrailingPathSeparator, inferProjectTitleFromPath, isExplicitRelativeProjectPath, - isFilesystemBrowseQuery, isUnsupportedWindowsProjectPath, resolveProjectPathForDispatch, } from "../lib/projectPaths"; @@ -92,13 +96,10 @@ import { buildProjectActionItems, buildRootGroups, buildThreadActionItems, - canPreloadBrowsePath, - createBrowseNavigationCoordinator, enumerateCommandPaletteItems, type CommandPaletteActionItem, type CommandPaletteSubmenuItem, type CommandPaletteView, - filterBrowseEntries, filterCommandPaletteGroups, getCommandPaletteInputPlaceholder, getCommandPaletteMode, @@ -164,6 +165,8 @@ interface AddProjectEnvironmentOption { readonly environmentId: EnvironmentId; readonly label: string; readonly isPrimary: boolean; + readonly isConnected: boolean; + readonly status: string; } type AddProjectRemoteProviderKind = Extract< @@ -623,6 +626,8 @@ function OpenCommandPaletteDialog(props: { runtimeLabel: environment.label, }), isPrimary, + isConnected: canCreateProjectInEnvironment(environment.connection.phase), + status: connectionStatusText(environment.connection), }; }); @@ -635,10 +640,14 @@ function OpenCommandPaletteDialog(props: { return options; }, [environments]); - const defaultAddProjectEnvironmentId = addProjectEnvironmentOptions[0]?.environmentId ?? null; + const defaultAddProjectEnvironmentId = + addProjectEnvironmentOptions.find((option) => option.isConnected)?.environmentId ?? null; const wslAddProjectEnvironmentOption = useMemo( () => addProjectEnvironmentOptions.find((option) => { + if (!option.isConnected) { + return false; + } const environment = environments.find( (candidate) => candidate.environmentId === option.environmentId, ); @@ -686,8 +695,12 @@ function OpenCommandPaletteDialog(props: { ); const isRemoteProjectCloneFlow = addProjectCloneFlow !== null; const isRemoteProjectRepositoryStep = addProjectCloneFlow?.step === "repository"; - const isBrowsing = - !isRemoteProjectRepositoryStep && isFilesystemBrowseQuery(query, browseEnvironmentPlatform); + const browsePath = useMemo( + () => getFilesystemBrowsePath(query, browseEnvironmentPlatform, !isRemoteProjectRepositoryStep), + [browseEnvironmentPlatform, isRemoteProjectRepositoryStep, query], + ); + const isBrowsing = browsePath.isBrowsing; + const browseDirectoryPath = browsePath.directoryPath; const paletteMode = getCommandPaletteMode({ currentView, isBrowsing }); const getAddProjectInitialQueryForEnvironment = useCallback( (environmentId: EnvironmentId | null): string => { @@ -732,18 +745,15 @@ function OpenCommandPaletteDialog(props: { ); const relativePathNeedsActiveProject = isExplicitRelativeProjectPath(query.trim()) && currentProjectCwdForBrowse === null; - const browseDirectoryPath = isBrowsing ? getBrowseDirectoryPath(query) : ""; - const browseFilterQuery = - isBrowsing && !hasTrailingPathSeparator(query) ? getBrowseLeafPathSegment(query) : ""; const browseQuery = useEnvironmentQuery( isBrowsing && - browseDirectoryPath.length > 0 && + browsePath.directoryPath.length > 0 && browseEnvironmentId !== null && !relativePathNeedsActiveProject ? filesystemEnvironment.browse({ environmentId: browseEnvironmentId, input: { - partialPath: browseDirectoryPath, + partialPath: browsePath.directoryPath, ...(currentProjectCwdForBrowse ? { cwd: currentProjectCwdForBrowse } : {}), }, }) @@ -752,9 +762,9 @@ function OpenCommandPaletteDialog(props: { const browseResult = browseQuery.data; const isBrowsePending = browseQuery.isPending; const browseEntries = browseResult?.entries ?? EMPTY_BROWSE_ENTRIES; - const { filteredEntries: filteredBrowseEntries, exactEntry: exactBrowseEntry } = useMemo( - () => filterBrowseEntries({ browseEntries, browseFilterQuery }), - [browseEntries, browseFilterQuery], + const { visibleEntries: visibleBrowseEntries, exactEntry: exactBrowseEntry } = useMemo( + () => filterFilesystemBrowseEntries(browseEntries, browsePath.filterQuery), + [browseEntries, browsePath.filterQuery], ); const prefetchBrowsePath = useCallback( @@ -975,17 +985,17 @@ function OpenCommandPaletteDialog(props: { initialQuery, }; - await browseNavigation.run({ - load: () => + await browseNavigation.run( + () => initialBrowsePath.length > 0 ? prefetchBrowsePath(initialBrowsePath, environmentId, browseCwd) : Promise.resolve(), - commit: () => { + () => { setAddProjectEnvironmentId(environmentId); setAddProjectCloneFlow(null); pushPaletteView(view); }, - }); + ); }, [ browseNavigation, @@ -1110,6 +1120,19 @@ function OpenCommandPaletteDialog(props: { const startAddProjectSourceSelection = useCallback( (environmentId: EnvironmentId): void => { + const environment = environments.find( + (candidate) => candidate.environmentId === environmentId, + ); + if (!canCreateProjectInEnvironment(environment?.connection.phase)) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Environment unavailable", + description: `${environment?.label ?? "The selected environment"} is not connected.`, + }), + ); + return; + } setAddProjectEnvironmentId(environmentId); setAddProjectCloneFlow(null); pushPaletteView({ @@ -1125,6 +1148,7 @@ function OpenCommandPaletteDialog(props: { [ browseEnvironmentId, buildAddProjectSourceGroups, + environments, pushPaletteView, sourceControlDiscovery.data, ], @@ -1136,7 +1160,12 @@ function OpenCommandPaletteDialog(props: { value: `action:add-project:environment:${option.environmentId}`, searchTerms: [option.label, option.environmentId, option.isPrimary ? "this device" : ""], title: option.label, - description: option.isPrimary ? "This device" : option.environmentId, + description: option.isConnected + ? option.isPrimary + ? "This device" + : option.environmentId + : option.status, + disabled: !option.isConnected, icon: , keepOpen: true, run: async () => { @@ -1157,7 +1186,7 @@ function OpenCommandPaletteDialog(props: { ); const openAddProjectFlow = useCallback(() => { - if (addProjectEnvironmentOptions.length > 1) { + if (addProjectEnvironmentOptions.length > 1 || defaultAddProjectEnvironmentId === null) { pushPaletteView({ addonIcon: , groups: addProjectEnvironmentGroups, @@ -1296,6 +1325,7 @@ function OpenCommandPaletteDialog(props: { "environment", ], title: "Add project", + disabled: defaultAddProjectEnvironmentId === null, icon: , keepOpen: true, run: async () => { @@ -1357,6 +1387,19 @@ function OpenCommandPaletteDialog(props: { readonly platform: string; readonly currentProjectCwd: string | null; }) => { + const environment = environments.find( + (candidate) => candidate.environmentId === input.environmentId, + ); + if (!canCreateProjectInEnvironment(environment?.connection.phase)) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Environment unavailable", + description: `${environment?.label ?? "The selected environment"} is not connected.`, + }), + ); + return; + } const rawCwd = input.rawCwd; if (isUnsupportedWindowsProjectPath(rawCwd.trim(), input.platform)) { @@ -1509,6 +1552,16 @@ function OpenCommandPaletteDialog(props: { if (!addProjectCloneFlow) { return; } + if (!canCreateProjectInEnvironment(browseEnvironment?.connection.phase)) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Environment unavailable", + description: `${browseEnvironment?.label ?? "The selected environment"} is not connected.`, + }), + ); + return; + } if (addProjectCloneFlow.step === "repository") { const rawRepository = query.trim(); @@ -1632,33 +1685,33 @@ function OpenCommandPaletteDialog(props: { const browseTo = useCallback( async (name: string): Promise => { const nextQuery = appendBrowsePathSegment(query, name); - await browseNavigation.run({ - load: () => prefetchBrowsePath(getBrowseDirectoryPath(nextQuery)), - commit: () => { + await browseNavigation.run( + () => prefetchBrowsePath(getBrowseDirectoryPath(nextQuery)), + () => { setHighlightedItemValue(null); setQuery(nextQuery); setBrowseGeneration((generation) => generation + 1); }, - }); + ); }, [browseNavigation, prefetchBrowsePath, query], ); const browseUp = useCallback(async (): Promise => { - const parentPath = getBrowseParentPath(query); + const parentPath = browsePath.parentPath; if (parentPath === null) { return; } - await browseNavigation.run({ - load: () => prefetchBrowsePath(parentPath), - commit: () => { + await browseNavigation.run( + () => prefetchBrowsePath(parentPath), + () => { setHighlightedItemValue(null); setQuery(parentPath); setBrowseGeneration((generation) => generation + 1); }, - }); - }, [browseNavigation, prefetchBrowsePath, query]); + ); + }, [browseNavigation, browsePath.parentPath, prefetchBrowsePath]); // Resolve the add-project path from browse data when available. When the // query has a trailing separator (e.g. "~/projects/foo/"), parentPath is the @@ -1668,11 +1721,10 @@ function OpenCommandPaletteDialog(props: { ? (browseResult?.parentPath ?? query.trim()) : (exactBrowseEntry?.fullPath ?? query.trim()); - const canBrowseUp = - isBrowsing && !relativePathNeedsActiveProject && canNavigateUp(browseDirectoryPath); + const canBrowseUp = !relativePathNeedsActiveProject && browsePath.canBrowseUp; const browseGroups = buildBrowseGroups({ - browseEntries: filteredBrowseEntries, + browseEntries: visibleBrowseEntries, browseQuery: query, canBrowseUp, upIcon: , @@ -1714,7 +1766,10 @@ function OpenCommandPaletteDialog(props: { getCommandPaletteInputPlaceholder(paletteMode); const isSubmenu = paletteMode === "submenu" || paletteMode === "submenu-browse"; const hasHighlightedBrowseItem = highlightedItemValue?.startsWith("browse:") ?? false; - const canSubmitBrowsePath = isBrowsing && !relativePathNeedsActiveProject; + const canSubmitBrowsePath = + isBrowsing && + !relativePathNeedsActiveProject && + canCreateProjectInEnvironment(browseEnvironment?.connection.phase); const willCreateProjectPath = canSubmitBrowsePath && !isBrowsePending && @@ -1741,6 +1796,7 @@ function OpenCommandPaletteDialog(props: { const canSubmitRemoteProjectFlow = addProjectCloneFlow?.step === "repository" && query.trim().length > 0 && + canCreateProjectInEnvironment(browseEnvironment?.connection.phase) && !isRemoteProjectPending; const fileManagerName = getLocalFileManagerName(navigator.platform); const canOpenProjectFromFileManager = @@ -2066,6 +2122,7 @@ function OpenCommandPaletteDialog(props: { )} aria-label={`${submitActionLabel} (${addShortcutLabel})`} disabled={ + !canCreateProjectInEnvironment(browseEnvironment?.connection.phase) || relativePathNeedsActiveProject || (isCloneDestinationStep && isRemoteProjectPending) } diff --git a/docs/architecture/connection-runtime.md b/docs/architecture/connection-runtime.md index 06f7e6338ca..acfd2ab5983 100644 --- a/docs/architecture/connection-runtime.md +++ b/docs/architecture/connection-runtime.md @@ -40,6 +40,9 @@ The supervisor is the only retry owner. seconds. 5. Connectivity changes, application activation, credential changes, and explicit user retry interrupt the current wait and trigger a fresh attempt. + Application activation also resets the backoff ladder. Mobile briefly probes + a session after short interruptions and replaces it immediately after a + meaningful background suspension. 6. Authentication or configuration failures remain blocked until an external wakeup changes the relevant input. 7. An involuntary session close keeps the registration and cache, then retries. diff --git a/packages/client-runtime/src/authorization/layer.test.ts b/packages/client-runtime/src/authorization/layer.test.ts index 1d2c6c6cca7..466a5c2dd4e 100644 --- a/packages/client-runtime/src/authorization/layer.test.ts +++ b/packages/client-runtime/src/authorization/layer.test.ts @@ -4,6 +4,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; +import * as TestClock from "effect/testing/TestClock"; import * as ManagedRelay from "../relay/managedRelay.ts"; import { remoteHttpClientLayer } from "../rpc/http.ts"; @@ -155,6 +156,81 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( }); describe("RemoteEnvironmentAuthorization", () => { + it.effect("reuses a validated bearer descriptor while issuing fresh websocket tickets", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + responses: [ + Response.json(DESCRIPTOR), + websocketTicket("first-ticket"), + websocketTicket("second-ticket"), + ], + }); + + const [first, second] = yield* Effect.gen(function* () { + const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; + const authorize = () => + remote.authorizeBearer({ + expectedEnvironmentId: ENVIRONMENT_ID, + httpBaseUrl: ENDPOINT.httpBaseUrl, + wsBaseUrl: ENDPOINT.wsBaseUrl, + bearerToken: "bearer-token", + }); + return [yield* authorize(), yield* authorize()] as const; + }).pipe(Effect.provide(harness.layer)); + + expect(first.socketUrl).toContain("wsTicket=first-ticket"); + expect(second.socketUrl).toContain("wsTicket=second-ticket"); + expect( + harness.fetch.calls.filter(([url]) => String(url).endsWith("/.well-known/t3/environment")), + ).toHaveLength(1); + expect( + harness.fetch.calls.filter(([url]) => String(url).endsWith("/api/auth/websocket-ticket")), + ).toHaveLength(2); + }), + ); + + it.effect("revalidates a bearer descriptor after the cache expires", () => + Effect.gen(function* () { + const reassignedEnvironmentId = EnvironmentId.make("environment-2"); + const harness = yield* makeHarness({ + responses: [ + Response.json(DESCRIPTOR), + websocketTicket("first-ticket"), + Response.json({ + ...DESCRIPTOR, + environmentId: reassignedEnvironmentId, + }), + ], + }); + + const failure = yield* Effect.gen(function* () { + const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; + const authorize = () => + remote.authorizeBearer({ + expectedEnvironmentId: ENVIRONMENT_ID, + httpBaseUrl: ENDPOINT.httpBaseUrl, + wsBaseUrl: ENDPOINT.wsBaseUrl, + bearerToken: "bearer-token", + }); + + yield* authorize(); + yield* TestClock.adjust("10 seconds"); + return yield* authorize().pipe(Effect.flip); + }).pipe(Effect.provide(Layer.merge(harness.layer, TestClock.layer()))); + + expect(failure).toEqual( + expect.objectContaining({ + _tag: "ConnectionBlockedError", + reason: "configuration", + detail: `Connected environment ${reassignedEnvironmentId} does not match ${ENVIRONMENT_ID}.`, + }), + ); + expect( + harness.fetch.calls.filter(([url]) => String(url).endsWith("/.well-known/t3/environment")), + ).toHaveLength(2); + }), + ); + it.effect("reuses a valid persisted environment token without contacting the relay", () => Effect.gen(function* () { const cached = new TokenStore.RemoteDpopAccessToken({ @@ -265,7 +341,7 @@ describe("RemoteEnvironmentAuthorization", () => { }), ); - it.effect("refreshes a cached endpoint after consecutive transient failures", () => + it.effect("refreshes a cached endpoint after its first transient failure", () => Effect.gen(function* () { const cached = new TokenStore.RemoteDpopAccessToken({ environmentId: ENVIRONMENT_ID, @@ -279,7 +355,6 @@ describe("RemoteEnvironmentAuthorization", () => { initialToken: cached, responses: [ new Response("endpoint unavailable", { status: 503 }), - new Response("endpoint still unavailable", { status: 503 }), Response.json(DESCRIPTOR), accessToken("replacement-access-token"), websocketTicket("replacement-ticket"), @@ -288,17 +363,6 @@ describe("RemoteEnvironmentAuthorization", () => { const authorized = yield* Effect.gen(function* () { const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; - const firstFailure = yield* remote - .authorizeDpop({ - expectedEnvironmentId: ENVIRONMENT_ID, - obtainBootstrap: harness.obtainBootstrap, - }) - .pipe(Effect.flip); - - expect(firstFailure._tag).toBe("ConnectionTransientError"); - expect(yield* Ref.get(harness.bootstrapCalls)).toBe(0); - expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)).toBe(cached); - return yield* remote.authorizeDpop({ expectedEnvironmentId: ENVIRONMENT_ID, obtainBootstrap: harness.obtainBootstrap, @@ -312,7 +376,7 @@ describe("RemoteEnvironmentAuthorization", () => { accessToken: "replacement-access-token", }), ); - expect(harness.fetch.calls).toHaveLength(5); + expect(harness.fetch.calls).toHaveLength(4); }), ); diff --git a/packages/client-runtime/src/authorization/service.ts b/packages/client-runtime/src/authorization/service.ts index 624ecf7f672..410c7a39dc0 100644 --- a/packages/client-runtime/src/authorization/service.ts +++ b/packages/client-runtime/src/authorization/service.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from "@t3tools/contracts"; +import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts"; import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; import { exchangeRemoteDpopAccessToken, @@ -58,7 +58,8 @@ export class RemoteEnvironmentAuthorization extends Context.Service< >()("@t3tools/client-runtime/authorization/service/RemoteEnvironmentAuthorization") {} const TOKEN_EXPIRY_SAFETY_MARGIN_MS = 60_000; -const CACHED_ENDPOINT_FAILURE_THRESHOLD = 2; +const CACHED_ENDPOINT_SOCKET_TIMEOUT_MS = 3_000; +const BEARER_DESCRIPTOR_CACHE_TTL_MS = 10_000; function mapDpopSocketError(error: RemoteEnvironmentAuthError | ConnectionAttemptError) { return error._tag === "ConnectionTransientError" || error._tag === "ConnectionBlockedError" @@ -79,25 +80,16 @@ export const make = Effect.gen(function* () { const presentation = yield* ClientCapabilities.ClientPresentation; const tokenStore = yield* TokenStore.RemoteDpopAccessTokenStore; const httpClient = yield* HttpClient.HttpClient; - const cachedEndpointFailures = yield* Ref.make>(new Map()); - - const resetCachedEndpointFailures = (environmentId: string) => - Ref.update(cachedEndpointFailures, (current) => { - if (!current.has(environmentId)) { - return current; + const bearerDescriptors = yield* Ref.make< + ReadonlyMap< + EnvironmentId, + { + readonly httpBaseUrl: string; + readonly descriptor: ExecutionEnvironmentDescriptor; + readonly validatedAtEpochMs: number; } - const next = new Map(current); - next.delete(environmentId); - return next; - }); - - const recordCachedEndpointFailure = (environmentId: string) => - Ref.modify(cachedEndpointFailures, (current) => { - const failureCount = (current.get(environmentId) ?? 0) + 1; - const next = new Map(current); - next.set(environmentId, failureCount); - return [failureCount, next] as const; - }); + > + >(new Map()); const authorizeBearer = Effect.fn("clientRuntime.connection.remote.authorizeBearer")( function* (input: { @@ -108,15 +100,33 @@ export const make = Effect.gen(function* () { readonly wsBaseUrl: string; readonly bearerToken: string; }) { - const descriptor = yield* fetchDescriptor(input.httpBaseUrl).pipe( - Effect.provideService(HttpClient.HttpClient, httpClient), - ); + const now = yield* Clock.currentTimeMillis; + const cachedDescriptor = (yield* Ref.get(bearerDescriptors)).get(input.expectedEnvironmentId); + const canReuseDescriptor = + cachedDescriptor?.httpBaseUrl === input.httpBaseUrl && + cachedDescriptor.validatedAtEpochMs + BEARER_DESCRIPTOR_CACHE_TTL_MS > now; + const descriptor = canReuseDescriptor + ? cachedDescriptor.descriptor + : yield* fetchDescriptor(input.httpBaseUrl).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + ); if (descriptor.environmentId !== input.expectedEnvironmentId) { return yield* environmentMismatchError({ expected: input.expectedEnvironmentId, actual: descriptor.environmentId, }); } + if (!canReuseDescriptor) { + yield* Ref.update(bearerDescriptors, (current) => { + const next = new Map(current); + next.set(input.expectedEnvironmentId, { + httpBaseUrl: input.httpBaseUrl, + descriptor, + validatedAtEpochMs: now, + }); + return next; + }); + } const socketUrl = yield* resolveRemoteWebSocketConnectionUrl({ wsBaseUrl: input.wsBaseUrl, httpBaseUrl: input.httpBaseUrl, @@ -139,7 +149,7 @@ export const make = Effect.gen(function* () { ); const createDpopSocketUrl = Effect.fn("clientRuntime.connection.remote.createDpopSocketUrl")( - function* (token: TokenStore.RemoteDpopAccessToken) { + function* (token: TokenStore.RemoteDpopAccessToken, timeoutMs?: number) { const ticketProof = yield* signer .createProof({ method: "POST", @@ -160,6 +170,7 @@ export const make = Effect.gen(function* () { httpBaseUrl: token.endpoint.httpBaseUrl, accessToken: token.accessToken, dpopProof: ticketProof, + ...(timeoutMs === undefined ? {} : { timeoutMs }), }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); }, ); @@ -196,9 +207,11 @@ export const make = Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ "connection.remote_token_cache": "hit", }); - const cachedSocket = yield* createDpopSocketUrl(cached.value).pipe(Effect.result); + const cachedSocket = yield* createDpopSocketUrl( + cached.value, + CACHED_ENDPOINT_SOCKET_TIMEOUT_MS, + ).pipe(Effect.result); if (Result.isSuccess(cachedSocket)) { - yield* resetCachedEndpointFailures(input.expectedEnvironmentId); return { environmentId: cached.value.environmentId, label: cached.value.label, @@ -213,20 +226,11 @@ export const make = Effect.gen(function* () { if (cachedSocket.failure._tag === "ConnectionBlockedError") { return yield* mapDpopSocketError(cachedSocket.failure); } - const mappedFailure = mapDpopSocketError(cachedSocket.failure); - if (mappedFailure._tag === "ConnectionTransientError") { - const failureCount = yield* recordCachedEndpointFailure(input.expectedEnvironmentId); - if (failureCount < CACHED_ENDPOINT_FAILURE_THRESHOLD) { - return yield* mappedFailure; - } - } yield* tokenStore .remove(input.expectedEnvironmentId) .pipe(Effect.withSpan("environment.authorization.accessToken.remove")); - yield* resetCachedEndpointFailures(input.expectedEnvironmentId); } - yield* resetCachedEndpointFailures(input.expectedEnvironmentId); yield* Effect.annotateCurrentSpan({ "connection.remote_token_cache": "miss", }); diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index f3901e42251..b31ea9b4fc9 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -124,7 +124,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: const releaseCount = yield* Ref.make(0); const wakeups = yield* SubscriptionRef.make<{ readonly sequence: number; - readonly reason: "application-active" | "credentials-changed"; + readonly reason: ConnectionWakeups.ConnectionWakeup; }>({ sequence: 0, reason: "application-active", @@ -198,7 +198,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: sessionCount, releaseCount, setNetworkStatus: (status: NetworkStatus) => SubscriptionRef.set(networkStatus, status), - wake: (reason: "application-active" | "credentials-changed") => + wake: (reason: ConnectionWakeups.ConnectionWakeup) => SubscriptionRef.update(wakeups, (event) => ({ sequence: event.sequence + 1, reason, @@ -311,6 +311,38 @@ describe("EnvironmentSupervisor", () => { }), ); + it.effect("resets retries when activation arrives before the network returns", () => + Effect.gen(function* () { + const harness = yield* makeHarness(); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* harness.setNetworkStatus("offline"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "offline" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "offline" && state.attempt === 1, + ); + yield* harness.setNetworkStatus("online"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2 && state.attempt === 1, + ); + }), + ); + it.effect("retries forever with exponential backoff capped at sixteen seconds", () => Effect.gen(function* () { const harness = yield* makeHarness({ @@ -501,6 +533,38 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect("resets retries when activation wakes a blocked connection", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + prepare: (attempt) => + attempt === 1 + ? Effect.fail(transient()) + : attempt === 2 + ? Effect.fail(blocked()) + : Effect.succeed(PREPARED_CONNECTION), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "blocked" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.attempt === 1, + ); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("releases a live session while offline and starts a new generation when online", () => Effect.gen(function* () { const harness = yield* makeHarness(); @@ -652,6 +716,101 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect("restarts the retry ladder when mobile returns to the foreground", () => + Effect.gen(function* () { + const harness = yield* makeHarness(); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2, + ); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 3 && state.attempt === 1, + ); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + + expect(yield* Ref.get(harness.sessionCount)).toBe(3); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("restarts the retry ladder when a long resume replaces a connected session", () => + Effect.gen(function* () { + const harness = yield* makeHarness(); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2 && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 3 && state.attempt === 1, + ); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("restarts the retry ladder when a long resume interrupts connection setup", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + prepare: (attempt) => (attempt === 2 ? Effect.never : Effect.succeed(PREPARED_CONNECTION)), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connecting" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2 && state.attempt === 1, + ); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("probes the active session without reconnecting on application activation", () => Effect.gen(function* () { const probeCount = yield* Ref.make(0); @@ -677,6 +836,58 @@ describe("EnvironmentSupervisor", () => { }), ); + it.effect("immediately replaces a mobile session after a long background resume", () => + Effect.gen(function* () { + const probeCount = yield* Ref.make(0); + const harness = yield* makeHarness({ + probe: () => Ref.update(probeCount, (count) => count + 1), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 1, + ); + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2, + ); + + expect(yield* Ref.get(probeCount)).toBe(0); + expect(yield* Ref.get(harness.sessionCount)).toBe(2); + expect(yield* Ref.get(harness.releaseCount)).toBe(1); + }), + ); + + it.effect("replaces a mobile session when a long resume interrupts an active probe", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + probe: (attempt) => (attempt === 1 ? Effect.never : Effect.void), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 1, + ); + yield* harness.wake("application-active-probe"); + yield* Effect.yieldNow; + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2, + ); + + expect(yield* Ref.get(harness.sessionCount)).toBe(2); + expect(yield* Ref.get(harness.releaseCount)).toBe(1); + }), + ); + it.effect("reconnects when the foreground liveness probe fails", () => Effect.gen(function* () { const harness = yield* makeHarness({ @@ -701,7 +912,7 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); - it.effect("times out a stalled foreground liveness probe and reconnects", () => + it.effect("quickly times out a stalled mobile foreground liveness probe", () => Effect.gen(function* () { const harness = yield* makeHarness({ probe: (attempt) => (attempt === 1 ? Effect.never : Effect.void), @@ -711,8 +922,8 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(harness.dependencies)); yield* awaitState(supervisor.state, (state) => state.phase === "connected"); - yield* harness.wake("application-active"); - yield* TestClock.adjust("15 seconds"); + yield* harness.wake("application-active-probe"); + yield* TestClock.adjust("3 seconds"); yield* awaitState( supervisor.state, (state) => state.phase === "backoff" && state.lastFailure?.reason === "timeout", diff --git a/packages/client-runtime/src/connection/supervisor.ts b/packages/client-runtime/src/connection/supervisor.ts index d9efcd4263a..e4ac359e5b1 100644 --- a/packages/client-runtime/src/connection/supervisor.ts +++ b/packages/client-runtime/src/connection/supervisor.ts @@ -32,6 +32,7 @@ import * as ConnectionWakeups from "./wakeups.ts"; const RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000] as const; const CONNECTION_ESTABLISHMENT_TIMEOUT = "15 seconds"; const CONNECTION_PROBE_TIMEOUT = "15 seconds"; +const MOBILE_CONNECTION_PROBE_TIMEOUT = "3 seconds"; const BACKOFF_RESET_AFTER_MS = 30_000; interface SupervisorIntent { @@ -63,6 +64,7 @@ type AttemptOutcome = readonly _tag: "Interrupted"; readonly established: boolean; readonly stable: boolean; + readonly resetRetry: boolean; } | { readonly _tag: "Failure"; @@ -82,7 +84,7 @@ type EstablishmentEvent = TracedAttemptFailure >; } - | { readonly _tag: "Interrupted" } + | { readonly _tag: "Interrupted"; readonly resetRetry: boolean } | { readonly _tag: "TimedOut" }; function exitUnlessInterrupted( @@ -168,7 +170,7 @@ function failureFromExit( stable: boolean, ): AttemptOutcome { if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) { - return { _tag: "Interrupted", established, stable }; + return { _tag: "Interrupted", established, stable, resetRetry: false }; } const typedFailure = exit.cause.reasons.find(Cause.isFailReason); if (typedFailure) { @@ -365,18 +367,21 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( switch (next._tag) { case "DisconnectRequested": case "RetryRequested": - return; + return false; case "NetworkChanged": if (next.network === "offline") { - return; + return false; } break; case "ConnectRequested": break; case "Wakeup": + if (next.reason === "application-active-reconnect") { + return true; + } if (next.reason === "credentials-changed" && target._tag === "RelayConnectionTarget") { yield* logManagedRelayAccountChange; - return; + return false; } break; } @@ -391,21 +396,30 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( switch (next._tag) { case "DisconnectRequested": case "RetryRequested": - return; + return false; case "NetworkChanged": if (next.network === "offline") { - return; + return false; } break; case "Wakeup": if (next.reason === "credentials-changed" && target._tag === "RelayConnectionTarget") { yield* logManagedRelayAccountChange; - return; + return false; + } + if (next.reason === "application-active-reconnect") { + // Mobile operating systems commonly suspend sockets without + // delivering a close event. A long background resume deliberately + // replaces that lease and starts a fresh attempt without backoff. + return true; } - if (next.reason === "application-active") { + if (next.reason === "application-active" || next.reason === "application-active-probe") { const probe = yield* lease.session.probe.pipe( Effect.timeoutOrElse({ - duration: CONNECTION_PROBE_TIMEOUT, + duration: + next.reason === "application-active-probe" + ? MOBILE_CONNECTION_PROBE_TIMEOUT + : CONNECTION_PROBE_TIMEOUT, orElse: () => Effect.fail( new ConnectionTransientError({ @@ -433,15 +447,27 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( case "DisconnectRequested": case "RetryRequested": yield* Fiber.interrupt(probe); - return; + return false; case "NetworkChanged": if (probeEvent.signal.network === "offline") { yield* Fiber.interrupt(probe); - return; + return false; } break; - case "ConnectRequested": case "Wakeup": + if (probeEvent.signal.reason === "application-active-reconnect") { + yield* Fiber.interrupt(probe); + return true; + } + if ( + probeEvent.signal.reason === "credentials-changed" && + target._tag === "RelayConnectionTarget" + ) { + yield* Fiber.interrupt(probe); + return false; + } + break; + case "ConnectRequested": break; } } @@ -471,7 +497,14 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( }), ), ), - waitForEstablishmentInterrupt().pipe(Effect.as({ _tag: "Interrupted" })), + waitForEstablishmentInterrupt().pipe( + Effect.map( + (resetRetry): EstablishmentEvent => ({ + _tag: "Interrupted", + resetRetry, + }), + ), + ), Effect.sleep(CONNECTION_ESTABLISHMENT_TIMEOUT).pipe( Effect.as({ _tag: "TimedOut" }), ), @@ -482,6 +515,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( _tag: "Interrupted", established: false, stable: false, + resetRetry: establishment.resetRetry, } satisfies AttemptOutcome; } if (establishment._tag === "TimedOut") { @@ -524,6 +558,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( _tag: "Interrupted", established: false, stable: false, + resetRetry: false, } satisfies AttemptOutcome; } @@ -560,42 +595,58 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( ), ).pipe(exitUnlessInterrupted); const connectedForMs = (yield* Clock.currentTimeMillis) - connectedAt; + if (Exit.isSuccess(connectedExit)) { + return { + _tag: "Interrupted", + established: true, + stable: connectedForMs >= BACKOFF_RESET_AFTER_MS, + resetRetry: connectedExit.value, + } satisfies AttemptOutcome; + } return failureFromExit(target, connectedExit, true, connectedForMs >= BACKOFF_RESET_AFTER_MS); }, Effect.ensuring(clearLease)); const waitForRetrySignal = Effect.fnUntraced(function* (delayMs: number) { return yield* Effect.raceFirst( - Effect.sleep(delayMs), + Effect.sleep(delayMs).pipe(Effect.as(false)), Effect.gen(function* () { for (;;) { const next = yield* Queue.take(signals); switch (next._tag) { + case "Wakeup": + return ConnectionWakeups.isApplicationActiveWakeup(next.reason); case "ConnectRequested": case "DisconnectRequested": case "RetryRequested": case "NetworkChanged": - case "Wakeup": - return; + return false; } } }), ); }); - const waitForSignal = Queue.take(signals); + const waitForSignal = Queue.take(signals).pipe( + Effect.map( + (next) => next._tag === "Wakeup" && ConnectionWakeups.isApplicationActiveWakeup(next.reason), + ), + ); const run = Effect.fnUntraced(function* () { let failureCount = 0; let generation = 0; let latestFailure: ConnectionAttemptError | null = null; let pendingRetry = Option.none(); + const resetRetryLadder = () => { + failureCount = 0; + pendingRetry = Option.none(); + }; for (;;) { const currentIntent = yield* Ref.get(intent); if (!currentIntent.desired) { - failureCount = 0; + resetRetryLadder(); latestFailure = null; - pendingRetry = Option.none(); yield* clearLease; yield* setState(availableState(currentIntent, generation)); yield* waitForSignal; @@ -604,7 +655,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( if (currentIntent.network === "offline") { yield* clearLease; yield* setState(offlineState(currentIntent, generation, failureCount + 1, latestFailure)); - yield* waitForSignal; + const applicationActivated = yield* waitForSignal; + if (applicationActivated) { + resetRetryLadder(); + } continue; } @@ -616,12 +670,14 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( if (outcome.established) { generation = nextGeneration; if (outcome.stable) { - failureCount = 0; + resetRetryLadder(); latestFailure = null; - pendingRetry = Option.none(); } } if (outcome._tag === "Interrupted") { + if (outcome.resetRetry) { + resetRetryLadder(); + } continue; } @@ -640,7 +696,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( lastFailure: error, retryAt: null, }); - yield* waitForSignal; + const applicationActivated = yield* waitForSignal; + if (applicationActivated) { + resetRetryLadder(); + } continue; } @@ -663,7 +722,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( lastFailure: error, retryAt: (yield* Clock.currentTimeMillis) + delayMs, }); - yield* waitForRetrySignal(delayMs); + const applicationActivated = yield* waitForRetrySignal(delayMs); + if (applicationActivated) { + resetRetryLadder(); + } } }); diff --git a/packages/client-runtime/src/connection/wakeups.ts b/packages/client-runtime/src/connection/wakeups.ts index 107c5983e02..8573a49c147 100644 --- a/packages/client-runtime/src/connection/wakeups.ts +++ b/packages/client-runtime/src/connection/wakeups.ts @@ -2,7 +2,23 @@ import * as Context from "effect/Context"; import * as Layer from "effect/Layer"; import type * as Stream from "effect/Stream"; -export type ConnectionWakeup = "application-active" | "credentials-changed"; +export type ConnectionWakeup = + | "application-active" + | "application-active-probe" + | "application-active-reconnect" + | "credentials-changed"; + +export function isApplicationActiveWakeup(reason: ConnectionWakeup): boolean { + return ( + reason === "application-active" || + reason === "application-active-probe" || + reason === "application-active-reconnect" + ); +} + +export function shouldResubscribeAfterWakeup(reason: ConnectionWakeup): boolean { + return reason === "application-active" || reason === "application-active-probe"; +} export class ConnectionWakeups extends Context.Service< ConnectionWakeups, diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 11b49742460..4cca703c145 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -10,6 +10,7 @@ import * as Option from "effect/Option"; import { buildAddProjectRemoteSourceReadiness, buildProjectCreateCommand, + canCreateProjectInEnvironment, findExistingAddProject, getAddProjectInitialQuery, resolveAddProjectPath, @@ -18,6 +19,15 @@ import { import type { EnvironmentProject } from "../state/models.ts"; describe("add project shared logic", () => { + it("only allows project creation in connected environments", () => { + expect(canCreateProjectInEnvironment("connected")).toBe(true); + expect(canCreateProjectInEnvironment("available")).toBe(false); + expect(canCreateProjectInEnvironment("offline")).toBe(false); + expect(canCreateProjectInEnvironment("connecting")).toBe(false); + expect(canCreateProjectInEnvironment("reconnecting")).toBe(false); + expect(canCreateProjectInEnvironment("error")).toBe(false); + }); + it("resolves initial browse paths from settings", () => { expect(getAddProjectInitialQuery("")).toBe("~/"); expect(getAddProjectInitialQuery("/work")).toBe("/work/"); diff --git a/packages/client-runtime/src/operations/projects.ts b/packages/client-runtime/src/operations/projects.ts index 6ae6e18baa2..056f96b21de 100644 --- a/packages/client-runtime/src/operations/projects.ts +++ b/packages/client-runtime/src/operations/projects.ts @@ -1,3 +1,4 @@ +import type { EnvironmentConnectionPhase } from "../connection/presentation.ts"; import type { CommandId, EnvironmentId, @@ -27,6 +28,12 @@ export type AddProjectRemoteProviderKind = Extract< >; export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; +export function canCreateProjectInEnvironment( + connectionPhase: EnvironmentConnectionPhase | null | undefined, +): boolean { + return connectionPhase === "connected"; +} + export type AddProjectRemoteSourceReadiness = Record< AddProjectRemoteSource, { readonly ready: boolean; readonly hint: string | null } diff --git a/packages/client-runtime/src/state/filesystem.test.ts b/packages/client-runtime/src/state/filesystem.test.ts new file mode 100644 index 00000000000..44e3df6ab26 --- /dev/null +++ b/packages/client-runtime/src/state/filesystem.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + canPreloadBrowsePath, + createBrowseNavigationCoordinator, + filterFilesystemBrowseEntries, + getFilesystemBrowsePath, +} from "./filesystem.ts"; + +describe("filesystem browse model", () => { + it("derives the browse target and navigation state", () => { + expect(getFilesystemBrowsePath("~/projects/t3")).toEqual({ + isBrowsing: true, + directoryPath: "~/projects/", + filterQuery: "t3", + parentPath: "~/", + canBrowseUp: true, + }); + expect(getFilesystemBrowsePath("C:\\Users\\test", "MacIntel").isBrowsing).toBe(false); + expect(getFilesystemBrowsePath("~/projects/", "", false).isBrowsing).toBe(false); + }); + + it("filters names, hidden directories, and exact matches consistently", () => { + const entries = [ + { name: ".config", fullPath: "/Users/test/.config" }, + { name: "Code", fullPath: "/Users/test/Code" }, + { name: "codething", fullPath: "/Users/test/codething" }, + ]; + + expect(filterFilesystemBrowseEntries(entries, "co")).toEqual({ + visibleEntries: entries.slice(1, 3), + exactEntry: null, + }); + expect(filterFilesystemBrowseEntries(entries, "").visibleEntries).toEqual(entries.slice(1)); + expect(filterFilesystemBrowseEntries(entries, ".").visibleEntries).toEqual(entries.slice(0, 1)); + expect(filterFilesystemBrowseEntries(entries, "Code").exactEntry).toEqual(entries[1]); + }); +}); + +describe("browse navigation", () => { + it("only commits the latest valid navigation", async () => { + const navigation = createBrowseNavigationCoordinator(); + const first = Promise.withResolvers(); + const second = Promise.withResolvers(); + const commits: string[] = []; + const commit = (name: string) => () => commits.push(name); + const firstRun = navigation.run(() => first.promise, commit("first")); + const secondRun = navigation.run(() => second.promise, commit("second")); + + second.resolve(); + await expect(secondRun).resolves.toBe(true); + first.resolve(); + await expect(firstRun).resolves.toBe(false); + + const invalidated = Promise.withResolvers(); + const invalidatedRun = navigation.run(() => invalidated.promise, commit("stale")); + navigation.invalidate(); + invalidated.resolve(); + + await expect(invalidatedRun).resolves.toBe(false); + expect(commits).toEqual(["second"]); + }); + + it("only preloads connected environments", () => { + expect(canPreloadBrowsePath("connected")).toBe(true); + expect(canPreloadBrowsePath("offline")).toBe(false); + expect(canPreloadBrowsePath("reconnecting")).toBe(false); + expect(canPreloadBrowsePath(null)).toBe(false); + }); +}); diff --git a/packages/client-runtime/src/state/filesystem.ts b/packages/client-runtime/src/state/filesystem.ts index c78b66cf316..794dc404147 100644 --- a/packages/client-runtime/src/state/filesystem.ts +++ b/packages/client-runtime/src/state/filesystem.ts @@ -1,8 +1,75 @@ -import { WS_METHODS } from "@t3tools/contracts"; +import { type FilesystemBrowseEntry, WS_METHODS } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; -import { createEnvironmentRpcQueryAtomFamily } from "./runtime.ts"; +import type { EnvironmentConnectionPhase } from "../connection/presentation.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; +import { + canNavigateUp, + getBrowseDirectoryPath, + getBrowseLeafPathSegment, + getBrowseParentPath, + hasTrailingPathSeparator, + isFilesystemBrowseQuery, +} from "./projects.ts"; +import { createEnvironmentRpcQueryAtomFamily } from "./runtime.ts"; + +export function getFilesystemBrowsePath(query: string, platform = "", enabled = true) { + const isBrowsing = enabled && isFilesystemBrowseQuery(query, platform); + const directoryPath = isBrowsing ? getBrowseDirectoryPath(query) : ""; + const filterQuery = + isBrowsing && !hasTrailingPathSeparator(query) ? getBrowseLeafPathSegment(query) : ""; + const parentPath = isBrowsing ? getBrowseParentPath(directoryPath) : null; + + return { + isBrowsing, + directoryPath, + filterQuery, + parentPath, + canBrowseUp: isBrowsing && canNavigateUp(directoryPath), + }; +} + +export function filterFilesystemBrowseEntries( + entries: ReadonlyArray, + query: string, +) { + const lowerQuery = query.toLowerCase(); + const showHidden = query.startsWith("."); + const visibleEntries = entries.filter( + (entry) => + entry.name.toLowerCase().startsWith(lowerQuery) && + (showHidden || !entry.name.startsWith(".")), + ); + const exactEntry = + query.length > 0 ? (visibleEntries.find((entry) => entry.name === query) ?? null) : null; + + return { visibleEntries, exactEntry }; +} + +export function createBrowseNavigationCoordinator() { + let generation = 0; + + return { + invalidate: () => { + generation += 1; + }, + run: async (load: () => Promise, commit: () => void) => { + const navigationGeneration = ++generation; + await load(); + if (navigationGeneration !== generation) { + return false; + } + commit(); + return true; + }, + }; +} + +export function canPreloadBrowsePath( + connectionPhase: EnvironmentConnectionPhase | null | undefined, +): boolean { + return connectionPhase === "connected"; +} export function createFilesystemEnvironmentAtoms( runtime: Atom.AtomRuntime, diff --git a/packages/client-runtime/src/state/shell-sync.test.ts b/packages/client-runtime/src/state/shell-sync.test.ts index 62d2a2c28b9..e006fc3cd76 100644 --- a/packages/client-runtime/src/state/shell-sync.test.ts +++ b/packages/client-runtime/src/state/shell-sync.test.ts @@ -329,6 +329,21 @@ describe("environment shell synchronization", () => { expect(yield* Ref.get(loaderCalls)).toBe(2); expect(yield* Ref.get(subscriptionCount)).toBe(2); + + yield* Queue.offer(wakeups, "application-active-probe"); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(subscriptionCount)) >= 3) break; + yield* Effect.yieldNow; + } + expect(yield* Ref.get(loaderCalls)).toBe(3); + expect(yield* Ref.get(subscriptionCount)).toBe(3); + + yield* Queue.offer(wakeups, "application-active-reconnect"); + for (let attempt = 0; attempt < 10; attempt += 1) { + yield* Effect.yieldNow; + } + expect(yield* Ref.get(loaderCalls)).toBe(3); + expect(yield* Ref.get(subscriptionCount)).toBe(3); }), ); }); diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts index 6ccb11797f5..a266af5f5f4 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts @@ -172,7 +172,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") const foregroundResubscriptions = Option.match(wakeups, { onNone: () => Stream.never, onSome: (service) => - service.changes.pipe(Stream.filter((reason) => reason === "application-active")), + service.changes.pipe(Stream.filter(ConnectionWakeups.shouldResubscribeAfterWakeup)), }); yield* setSynchronizing; diff --git a/packages/client-runtime/src/state/threadSettled.test.ts b/packages/client-runtime/src/state/threadSettled.test.ts index 421d95c6b13..99d906c00fa 100644 --- a/packages/client-runtime/src/state/threadSettled.test.ts +++ b/packages/client-runtime/src/state/threadSettled.test.ts @@ -163,32 +163,11 @@ describe("effectiveSettled", () => { ).toBe(true); }); - it("does not re-settle a warm thread on the merge signal: a message sent in a settled thread keeps it active until idle", () => { - // The merge signal never clears, so without the idle guard a follow-up - // message would un-settle the row only until its turn completed, then - // snap straight back into the settled tail. - const justActive = makeShell({ activityAt: "2026-04-09T23:30:00.000Z" }); - // The idle gate is strict: activity exactly one hour old is still warm. - const boundary = makeShell({ activityAt: "2026-04-09T23:00:00.000Z" }); - const idle = makeShell({ activityAt: "2026-04-09T22:59:59.999Z" }); - + it("settles immediately when a change request merges or closes", () => { + const recentlyActive = makeShell({ activityAt: "2026-04-09T23:59:59.999Z" }); for (const changeRequestState of ["merged", "closed"] as const) { expect( - effectiveSettled(justActive, { - now: NOW, - autoSettleAfterDays: null, - changeRequestState, - }), - ).toBe(false); - expect( - effectiveSettled(boundary, { - now: NOW, - autoSettleAfterDays: null, - changeRequestState, - }), - ).toBe(false); - expect( - effectiveSettled(idle, { + effectiveSettled(recentlyActive, { now: NOW, autoSettleAfterDays: null, changeRequestState, @@ -197,14 +176,18 @@ describe("effectiveSettled", () => { } }); - it("re-settles a merged-PR thread once the follow-up burst goes idle", () => { - // Same shell, advancing clock: active while warm, settled again after - // the idle window passes — the burst cools and the merge signal wins. - const shell = makeShell({ activityAt: "2026-04-09T23:30:00.000Z" }); - const options = { autoSettleAfterDays: null, changeRequestState: "merged" as const }; - - expect(effectiveSettled(shell, { ...options, now: NOW })).toBe(false); - expect(effectiveSettled(shell, { ...options, now: "2026-04-10T00:30:00.001Z" })).toBe(true); + it("keeps an explicitly un-settled merged-PR thread active", () => { + const shell = makeShell({ + settledOverride: "active", + activityAt: "2026-04-09T23:59:59.999Z", + }); + expect( + effectiveSettled(shell, { + now: NOW, + autoSettleAfterDays: null, + changeRequestState: "merged", + }), + ).toBe(false); }); it("never settles a starting session, even with a settled override", () => { diff --git a/packages/client-runtime/src/state/threadSettled.ts b/packages/client-runtime/src/state/threadSettled.ts index 0d077c892bb..3a98b9d2f27 100644 --- a/packages/client-runtime/src/state/threadSettled.ts +++ b/packages/client-runtime/src/state/threadSettled.ts @@ -214,20 +214,6 @@ export function threadWokeAt( return wakeAtMs <= Date.parse(options.now) ? shell.snoozedUntil : null; } -/** - * A merged/closed change request settles its thread only once the thread has - * been idle this long. Without the idle guard the merge signal is permanent: - * sending a message to a merged-PR thread would un-settle the row only until - * its turn completed, then the still-merged PR would snap it straight back - * into the settled tail. An hour keeps the follow-up conversation visible - * while it is warm; once the burst goes stale the merge signal settles it - * again. Activity timestamps can originate on another device while `now` is - * this caller's clock: skew shortens or stretches the window by its size, - * the same exposure the inactivity auto-settle already accepts — worst case - * is a row changing lists early or late, never lost work. - */ -export const CHANGE_REQUEST_SETTLE_IDLE_MS = 60 * 60 * 1_000; - /** * Settled resolution over the server-backed settled lifecycle. Activity * blockers (pending approval/user-input, a live session, an unadjudicated @@ -235,7 +221,7 @@ export const CHANGE_REQUEST_SETTLE_IDLE_MS = 60 * 60 * 1_000; * override. Past the blockers, the explicit user override (thread.settle / * thread.unsettle commands, projected into settledOverride + settledAt) * wins in both directions; without one, a thread auto-settles on a - * merged/closed PR (once idle) or inactivity past the window. The server + * merged/closed PR immediately or on inactivity past the window. The server * un-settles on real activity (user message, session start, approval/ * user-input request), so an override never goes stale silently. */ @@ -271,16 +257,7 @@ export function effectiveSettled( // until real activity clears it server-side. if (shell.settledOverride === "active") return false; if (options.changeRequestState === "merged" || options.changeRequestState === "closed") { - // Only an idle thread settles on the merge signal: the signal itself - // never clears, so without this guard fresh activity (a message sent in - // a settled thread) would re-settle the moment its turn completed. - const lastActivityAt = threadLastActivityAt(shell); - if ( - lastActivityAt === null || - Date.parse(lastActivityAt) < Date.parse(options.now) - CHANGE_REQUEST_SETTLE_IDLE_MS - ) { - return true; - } + return true; } if (options.autoSettleAfterDays === null) return false; diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index f2116080376..ff726532bb8 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -699,6 +699,19 @@ describe("EnvironmentThreads", () => { (value) => value.status === "live" && Option.isSome(value.data), ); expect(Option.getOrThrow(live.data).title).toBe("Latest title"); + + yield* Queue.offer(harness.wakeups, "application-active-probe"); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(harness.subscriptionCount)) >= 3) break; + yield* Effect.yieldNow; + } + expect(yield* Ref.get(harness.subscriptionCount)).toBe(3); + + yield* Queue.offer(harness.wakeups, "application-active-reconnect"); + for (let attempt = 0; attempt < 10; attempt += 1) { + yield* Effect.yieldNow; + } + expect(yield* Ref.get(harness.subscriptionCount)).toBe(3); }), ); }); diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index 196229cc8b1..06b5428ca58 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -236,7 +236,7 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make const foregroundResubscriptions = Option.match(wakeups, { onNone: () => Stream.never, onSome: (service) => - service.changes.pipe(Stream.filter((reason) => reason === "application-active")), + service.changes.pipe(Stream.filter(ConnectionWakeups.shouldResubscribeAfterWakeup)), }); yield* setSynchronizing; diff --git a/patches/@legendapp__list@3.2.0.patch b/patches/@legendapp__list@3.3.3.patch similarity index 79% rename from patches/@legendapp__list@3.2.0.patch rename to patches/@legendapp__list@3.3.3.patch index 686ea249b7a..4fa135d5aa0 100644 --- a/patches/@legendapp__list@3.2.0.patch +++ b/patches/@legendapp__list@3.3.3.patch @@ -1,8 +1,8 @@ diff --git a/keyboard.d.ts b/keyboard.d.ts -index 5a115ea..2c65d31 100644 +index 7bc3bb8..75ec120 100644 --- a/keyboard.d.ts +++ b/keyboard.d.ts -@@ -269,7 +269,7 @@ type KeyboardChatComposerInsetListRef = { +@@ -277,7 +277,7 @@ type KeyboardChatComposerInsetListRef = { type KeyboardChatComposerRef = { current: Pick | null; }; @@ -11,7 +11,7 @@ index 5a115ea..2c65d31 100644 contentInsetEndAdjustment: SharedValue; onComposerLayout: (event: LayoutChangeEvent) => void; }; -@@ -278,8 +278,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb +@@ -286,8 +286,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb scrollMessageToEnd: ({ animated, closeKeyboard }: ScrollMessageToEndOptions) => Promise; }; declare const KeyboardAwareLegendList: (props: Omit, "anchoredEndSpace" | "contentInsetEndAdjustment" | "renderScrollComponent"> & KeyboardChatScrollViewPropsUnique & { @@ -187,10 +187,10 @@ index c1dd270..cb0d142 100644 renderScrollComponent: memoList, ...rest diff --git a/react-native.d.ts b/react-native.d.ts -index 72d3f59..435a5fc 100644 +index 8204015..cdeaab7 100644 --- a/react-native.d.ts +++ b/react-native.d.ts -@@ -284,6 +284,12 @@ interface LegendListSpecificProps { +@@ -293,6 +293,12 @@ interface LegendListSpecificProps { * The adjustment is also rendered as real content padding so the browser scroll range includes it. */ contentInsetEndAdjustment?: number; @@ -204,10 +204,10 @@ index 72d3f59..435a5fc 100644 * Number of columns to render items in. * @default 1 diff --git a/react-native.js b/react-native.js -index 8d4ff89..18f0d62 100644 +index 229f09a..2a1ceb6 100644 --- a/react-native.js +++ b/react-native.js -@@ -1195,7 +1195,7 @@ function setInitialRenderState(ctx, { +@@ -930,7 +930,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -215,8 +215,8 @@ index 8d4ff89..18f0d62 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal"); -@@ -1480,18 +1480,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal", "ready"); +@@ -1259,18 +1259,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -242,7 +242,7 @@ index 8d4ff89..18f0d62 100644 return clampedOffset; } -@@ -1626,10 +1631,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1406,10 +1411,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -255,7 +255,7 @@ index 8d4ff89..18f0d62 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1676,7 +1681,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1456,7 +1461,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -267,10 +267,10 @@ index 8d4ff89..18f0d62 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1737,9 +1745,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1517,9 +1525,18 @@ function doMaintainScrollAtEnd(ctx) { } - state.pendingMaintainScrollAtEnd = false; if (shouldMaintainScrollAtEnd) { + state.pendingMaintainScrollAtEnd = false; + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { @@ -287,7 +287,7 @@ index 8d4ff89..18f0d62 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1759,9 +1776,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1539,9 +1556,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -309,7 +309,18 @@ index 8d4ff89..18f0d62 100644 } setTimeout( () => { -@@ -1888,7 +1914,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -1571,6 +1597,10 @@ function doMaintainScrollAtEnd(ctx) { + function requestAdjust(ctx, positionDiff, dataChanged) { + const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { ++ // Timestamp for ReanimatedPositionView: repositions caused by an MVCP ++ // size adjustment are already compensated by a contentOffset shift, so ++ // animating them would make rows visibly lurch and slide back. ++ state.lastMVCPAdjustTime = Date.now(); + const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; + const doit = () => { + if (needsScrollWorkaround) { +@@ -1674,7 +1704,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -320,7 +331,7 @@ index 8d4ff89..18f0d62 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1950,7 +1978,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1736,7 +1768,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -329,7 +340,7 @@ index 8d4ff89..18f0d62 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -2083,7 +2111,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1869,7 +1901,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -338,7 +349,7 @@ index 8d4ff89..18f0d62 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2374,8 +2402,121 @@ function scrollToIndex(ctx, { +@@ -2274,8 +2306,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -460,7 +471,7 @@ index 8d4ff89..18f0d62 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2804,7 +2945,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2704,7 +2849,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -471,7 +482,7 @@ index 8d4ff89..18f0d62 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4646,7 +4789,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4637,7 +4784,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -481,7 +492,7 @@ index 8d4ff89..18f0d62 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4664,6 +4808,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4655,6 +4803,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { updateScroll(ctx, state.scroll, true); } (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); @@ -494,7 +505,7 @@ index 8d4ff89..18f0d62 100644 } return nextSize; } -@@ -6462,6 +6612,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6960,6 +7114,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -502,23 +513,32 @@ index 8d4ff89..18f0d62 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -6492,6 +6643,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6990,6 +7145,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout: onLayoutProp, onLoad, onMomentumScrollEnd, + onScrollBeginDrag, onRefresh, onScroll: onScrollProp, - onStartReached, -@@ -6710,6 +6862,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag, +@@ -7076,7 +7232,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + const combinedRef = useCombinedRef(refScroller, refScrollView); + const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); + const stickyHeaderIndices = stickyHeaderIndicesProp; +- const contentInsetEndAdjustmentResolved = Platform.OS === "web" ? contentInsetEndAdjustment : void 0; ++ const contentInsetEndAdjustmentResolved = contentInsetEndAdjustment; + const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); + const alwaysRenderIndices = React2.useMemo(() => { + const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); +@@ -7215,6 +7371,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, + dataKey, dataVersion, - drawDistance, -@@ -6789,6 +6942,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7303,6 +7460,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -532,20 +552,15 @@ index 8d4ff89..18f0d62 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); React2.useLayoutEffect(() => { -@@ -6995,6 +7155,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onMomentumScrollEnd(event); - } - }, -+ onScrollBeginDrag: (event) => { +@@ -7526,6 +7690,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScroll: (event) => onScroll(ctx, event), + onScrollBeginDrag: (event) => { + var _a4, _b2; + ctx.state.didUserDrag = true; -+ if (onScrollBeginDrag) { -+ onScrollBeginDrag(event); -+ } -+ }, - onScroll: (event) => onScroll(ctx, event) - }), - [] -@@ -7019,6 +7185,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + prepareReachedEdgeForNextUserScroll(ctx); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, +@@ -7555,6 +7720,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -554,10 +569,10 @@ index 8d4ff89..18f0d62 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { diff --git a/react-native.mjs b/react-native.mjs -index 2e96ca7..6e8913e 100644 +index c2e0f38..5313086 100644 --- a/react-native.mjs +++ b/react-native.mjs -@@ -1174,7 +1174,7 @@ function setInitialRenderState(ctx, { +@@ -909,7 +909,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -565,8 +580,8 @@ index 2e96ca7..6e8913e 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal"); -@@ -1459,18 +1459,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal", "ready"); +@@ -1238,18 +1238,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -592,7 +607,7 @@ index 2e96ca7..6e8913e 100644 return clampedOffset; } -@@ -1605,10 +1610,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1385,10 +1390,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -605,7 +620,7 @@ index 2e96ca7..6e8913e 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1655,7 +1660,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1435,7 +1440,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -617,10 +632,10 @@ index 2e96ca7..6e8913e 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1716,9 +1724,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1496,9 +1504,18 @@ function doMaintainScrollAtEnd(ctx) { } - state.pendingMaintainScrollAtEnd = false; if (shouldMaintainScrollAtEnd) { + state.pendingMaintainScrollAtEnd = false; + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { @@ -637,7 +652,7 @@ index 2e96ca7..6e8913e 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1738,9 +1755,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1518,9 +1535,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -659,7 +674,18 @@ index 2e96ca7..6e8913e 100644 } setTimeout( () => { -@@ -1867,7 +1893,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -1550,6 +1576,10 @@ function doMaintainScrollAtEnd(ctx) { + function requestAdjust(ctx, positionDiff, dataChanged) { + const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { ++ // Timestamp for ReanimatedPositionView: repositions caused by an MVCP ++ // size adjustment are already compensated by a contentOffset shift, so ++ // animating them would make rows visibly lurch and slide back. ++ state.lastMVCPAdjustTime = Date.now(); + const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; + const doit = () => { + if (needsScrollWorkaround) { +@@ -1653,7 +1683,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -670,7 +696,7 @@ index 2e96ca7..6e8913e 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1929,7 +1957,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1715,7 +1747,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -679,7 +705,7 @@ index 2e96ca7..6e8913e 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -2062,7 +2090,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1848,7 +1880,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -688,7 +714,7 @@ index 2e96ca7..6e8913e 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2353,8 +2381,121 @@ function scrollToIndex(ctx, { +@@ -2253,8 +2285,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -810,7 +836,7 @@ index 2e96ca7..6e8913e 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2783,7 +2924,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2683,7 +2828,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -821,7 +847,7 @@ index 2e96ca7..6e8913e 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4625,7 +4768,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4616,7 +4763,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -831,7 +857,7 @@ index 2e96ca7..6e8913e 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4643,6 +4787,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4634,6 +4782,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { updateScroll(ctx, state.scroll, true); } (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); @@ -844,7 +870,7 @@ index 2e96ca7..6e8913e 100644 } return nextSize; } -@@ -6441,6 +6591,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6939,6 +7093,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -852,23 +878,24 @@ index 2e96ca7..6e8913e 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -6471,6 +6622,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onLayout: onLayoutProp, - onLoad, - onMomentumScrollEnd, -+ onScrollBeginDrag, - onRefresh, - onScroll: onScrollProp, - onStartReached, -@@ -6689,6 +6841,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7055,7 +7210,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + const combinedRef = useCombinedRef(refScroller, refScrollView); + const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); + const stickyHeaderIndices = stickyHeaderIndicesProp; +- const contentInsetEndAdjustmentResolved = Platform.OS === "web" ? contentInsetEndAdjustment : void 0; ++ const contentInsetEndAdjustmentResolved = contentInsetEndAdjustment; + const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); + const alwaysRenderIndices = useMemo(() => { + const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); +@@ -7194,6 +7349,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, + dataKey, dataVersion, - drawDistance, -@@ -6768,6 +6921,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7282,6 +7438,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -882,20 +909,15 @@ index 2e96ca7..6e8913e 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); useLayoutEffect(() => { -@@ -6974,6 +7134,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onMomentumScrollEnd(event); - } - }, -+ onScrollBeginDrag: (event) => { +@@ -7505,6 +7668,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScroll: (event) => onScroll(ctx, event), + onScrollBeginDrag: (event) => { + var _a4, _b2; + ctx.state.didUserDrag = true; -+ if (onScrollBeginDrag) { -+ onScrollBeginDrag(event); -+ } -+ }, - onScroll: (event) => onScroll(ctx, event) - }), - [] -@@ -6998,6 +7164,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + prepareReachedEdgeForNextUserScroll(ctx); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, +@@ -7534,6 +7698,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -904,10 +926,10 @@ index 2e96ca7..6e8913e 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { diff --git a/reanimated.d.ts b/reanimated.d.ts -index 7e2d11f..d5b0d66 100644 +index 940da28..28dccbe 100644 --- a/reanimated.d.ts +++ b/reanimated.d.ts -@@ -285,6 +285,12 @@ interface LegendListSpecificProps { +@@ -294,6 +294,12 @@ interface LegendListSpecificProps { * The adjustment is also rendered as real content padding so the browser scroll range includes it. */ contentInsetEndAdjustment?: number; @@ -920,3 +942,73 @@ index 7e2d11f..d5b0d66 100644 /** * Number of columns to render items in. * @default 1 +diff --git a/reanimated.js b/reanimated.js +index f1265fa..16dcef0 100644 +--- a/reanimated.js ++++ b/reanimated.js +@@ -116,7 +116,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); + const prevItemKeyRef = React__namespace.useRef(void 0); + let shouldSkipTransitionForRecycleReuse = false; +- if (recycleItems && layoutTransition) { ++ if (layoutTransition) { + const itemKeySignal = `containerItemKey${id}`; + const itemKey = peek$(ctx, itemKeySignal); + shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; +@@ -130,10 +130,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + () => [style, horizontal ? { left: positionValue } : { top: positionValue }], + [horizontal, positionValue, style] + ); ++ // Two kinds of repositions must not animate: (1) MVCP size adjustments, ++ // which are compensated by an equal contentOffset shift so the row should ++ // not visibly move — animating turns the invisible correction into a ++ // lurch-and-settle; (2) any reposition while the user is actively ++ // scrolling, where rows shifting under the finger reads as jank. The ++ // transition exists to smooth at-rest shifts (streaming text, work-log ++ // folds), so gate it to at-rest moments. ++ const now = Date.now(); ++ const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || ++ now - (ctx.state.lastNativeScrollTime || 0) < 300; + return /* @__PURE__ */ React__namespace.createElement( + Reanimated__default.default.View, + { +- layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, ++ layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, + ref: refView, + style: viewStyle, + ...rest +diff --git a/reanimated.mjs b/reanimated.mjs +index 29a00d5..9c25ec5 100644 +--- a/reanimated.mjs ++++ b/reanimated.mjs +@@ -92,7 +92,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); + const prevItemKeyRef = React.useRef(void 0); + let shouldSkipTransitionForRecycleReuse = false; +- if (recycleItems && layoutTransition) { ++ if (layoutTransition) { + const itemKeySignal = `containerItemKey${id}`; + const itemKey = peek$(ctx, itemKeySignal); + shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; +@@ -106,10 +106,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + () => [style, horizontal ? { left: positionValue } : { top: positionValue }], + [horizontal, positionValue, style] + ); ++ // Two kinds of repositions must not animate: (1) MVCP size adjustments, ++ // which are compensated by an equal contentOffset shift so the row should ++ // not visibly move — animating turns the invisible correction into a ++ // lurch-and-settle; (2) any reposition while the user is actively ++ // scrolling, where rows shifting under the finger reads as jank. The ++ // transition exists to smooth at-rest shifts (streaming text, work-log ++ // folds), so gate it to at-rest moments. ++ const now = Date.now(); ++ const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || ++ now - (ctx.state.lastNativeScrollTime || 0) < 300; + return /* @__PURE__ */ React.createElement( + Reanimated.View, + { +- layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, ++ layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, + ref: refView, + style: viewStyle, + ...rest diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61fceedc456..fd8fa90c94f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,7 +74,7 @@ patchedDependencies: '@effect/vitest@4.0.0-beta.102': a607339aab944136a084a05f159aa0f5a69776934d4b835dab8f9992f1c13425 '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 - '@legendapp/list@3.2.0': 45e4cbcbeeca1b628b8480869374d7cbc77e3fbfa97ee0107a0d72c3c4a5d7f3 + '@legendapp/list@3.3.3': d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09 '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 @@ -198,25 +198,25 @@ importers: dependencies: '@callstack/liquid-glass': specifier: ^0.7.1 - version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@clerk/expo': specifier: 4.0.2 - version: 4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3) + version: 4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) '@effect/atom-react': specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488))(react@19.2.6)(scheduler@0.27.0) + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488))(react@19.2.3)(scheduler@0.27.0) '@expo-google-fonts/dm-sans': specifier: ^0.4.2 version: 0.4.2 '@expo/metro-runtime': specifier: ~56.0.15 - version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/ui': specifier: ~56.0.18 - version: 56.0.18(32843e0c0883df8bccfa0b8323659df5) + version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) '@legendapp/list': - specifier: 3.2.0 - version: 3.2.0(patch_hash=45e4cbcbeeca1b628b8480869374d7cbc77e3fbfa97ee0107a0d72c3c4a5d7f3)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + specifier: 3.3.3 + version: 3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@noble/curves': specifier: 'catalog:' version: 1.9.1 @@ -225,19 +225,19 @@ importers: version: 1.8.0 '@pierre/diffs': specifier: 'catalog:' - version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-menu/menu': specifier: ^2.0.0 - version: 2.0.0(patch_hash=5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 2.0.0(patch_hash=5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@react-navigation/elements': specifier: 2.9.26 - version: 2.9.26(773cc3c3d7b6f6f948921e6a125849bf) + version: 2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4) '@react-navigation/native': specifier: 7.3.4 - version: 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@react-navigation/native-stack': specifier: 7.17.6 - version: 7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(452cd56f59ca0a03ec356fcc4ca0055e) + version: 7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(b7ff50e3b1e09b1815881c384829e9f8) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -258,7 +258,7 @@ importers: version: link:../../packages/contracts '@t3tools/mobile-markdown-text': specifier: file:./modules/t3-markdown-text - version: file:apps/mobile/modules/t3-markdown-text(2a2e612476bcb1a5250beb3b8fa0a80c) + version: file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe) '@t3tools/mobile-review-diff-native': specifier: file:./modules/t3-review-diff version: file:apps/mobile/modules/t3-review-diff @@ -270,7 +270,7 @@ importers: version: link:../../packages/shared '@tabler/icons-react-native': specifier: ^3.44.0 - version: 3.44.0(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react@19.2.6) + version: 3.44.0(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react@19.2.3) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -282,64 +282,64 @@ importers: version: 4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488) expo: specifier: ~56.0.12 - version: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + version: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-asset: specifier: ~56.0.17 - version: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3) + version: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-auth-session: specifier: ~56.0.14 - version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-blur: specifier: ~56.0.3 - version: 56.0.3(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.3(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-build-properties: specifier: ~56.0.19 version: 56.0.19(expo@56.0.12) expo-camera: specifier: ~56.0.8 - version: 56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-clipboard: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-constants: specifier: ~56.0.18 - version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) expo-dev-client: specifier: ~56.0.20 - version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-file-system: specifier: ~56.0.8 - version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-font: specifier: ~56.0.7 - version: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-glass-effect: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-haptics: specifier: ~56.0.3 version: 56.0.3(expo@56.0.12) expo-image: specifier: ~56.0.11 - version: 56.0.11(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.11(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-image-picker: specifier: ~56.0.18 version: 56.0.18(expo@56.0.12) expo-linking: specifier: ~56.0.14 - version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-network: specifier: ~56.0.5 - version: 56.0.5(expo@56.0.12)(react@19.2.6) + version: 56.0.5(expo@56.0.12)(react@19.2.3) expo-notifications: specifier: ~56.0.18 - version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3) + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-paste-input: specifier: ^0.1.15 - version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-quick-actions: specifier: ^6.0.2 version: 6.0.2(expo@56.0.12)(typescript@6.0.3) @@ -348,73 +348,73 @@ importers: version: 56.0.4(expo@56.0.12) expo-sharing: specifier: ~56.0.18 - version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3) + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-splash-screen: specifier: ~56.0.10 version: 56.0.10(expo@56.0.12)(typescript@6.0.3) expo-sqlite: specifier: ~56.0.5 - version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-symbols: specifier: ~56.0.6 - version: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-updates: specifier: ~56.0.19 - version: 56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-web-browser: specifier: ~56.0.5 - version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: specifier: ~56.0.19 - version: 56.0.19(32843e0c0883df8bccfa0b8323659df5) + version: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) punycode: specifier: ^2.3.1 version: 2.3.1 react: - specifier: 19.2.6 - version: 19.2.6 + specifier: 19.2.3 + version: 19.2.3 react-dom: - specifier: 19.2.6 - version: 19.2.6(react@19.2.6) + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) react-native: specifier: 0.85.3 - version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-gesture-handler: specifier: ~2.31.1 - version: 2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-image-viewing: specifier: ^0.2.2 - version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-keyboard-controller: specifier: 1.21.13 - version: 1.21.13(patch_hash=20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008)(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 1.21.13(patch_hash=20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008)(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-markdown: specifier: ^0.5.0 - version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-modules: specifier: 0.35.9 - version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-reanimated: specifier: 4.3.1 - version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-safe-area-context: specifier: ~5.7.0 - version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(patch_hash=282d02fa85a8a643548a27565dedd92bd3a729742be5c0371b0c4f6d07e33ce3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 4.25.2(patch_hash=282d02fa85a8a643548a27565dedd92bd3a729742be5c0371b0c4f6d07e33ce3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 - version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-svg: specifier: 15.15.4 - version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-webview: specifier: ^13.16.1 - version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-worklets: specifier: 0.8.3 - version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) shiki: specifier: 4.2.0 version: 4.2.0 @@ -423,14 +423,14 @@ importers: version: 3.6.0 uniwind: specifier: ^1.6.2 - version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(tailwindcss@4.3.0) + version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0) devDependencies: '@effect/vitest': specifier: 4.0.0-beta.102 version: 4.0.0-beta.102(patch_hash=a607339aab944136a084a05f159aa0f5a69776934d4b835dab8f9992f1c13425)(effect@4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488)) '@pierre/trees': specifier: 1.0.0-beta.4 - version: 1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.0.0-beta.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/react': specifier: ~19.2.0 version: 19.2.16 @@ -563,7 +563,7 @@ importers: version: 0.9.0 '@legendapp/list': specifier: 3.2.0 - version: 3.2.0(patch_hash=45e4cbcbeeca1b628b8480869374d7cbc77e3fbfa97ee0107a0d72c3c4a5d7f3)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + version: 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@lexical/react': specifier: ^0.41.0 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) @@ -3106,6 +3106,18 @@ packages: react-native: optional: true + '@legendapp/list@3.3.3': + resolution: {integrity: sha512-p3g4xG6f//s4XQKhuus2189GCQgOHEIbJXHePqeDxj+6UQQQyij4YBjyArNSCgqoP0c03sxDPSOuCFB128Ql6g==} + peerDependencies: + react: '*' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + '@lexical/clipboard@0.41.0': resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==} @@ -9025,6 +9037,11 @@ packages: react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + react-dom@19.2.6: resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: @@ -9228,6 +9245,10 @@ packages: '@types/react': optional: true + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + react@19.2.6: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} @@ -11617,10 +11638,10 @@ snapshots: '@bufbuild/protobuf@1.10.0': {} - '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': + '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@capsizecss/unpack@4.0.1': dependencies: @@ -11658,6 +11679,23 @@ snapshots: - react - react-dom + '@clerk/clerk-js@6.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@stripe/stripe-js': 5.6.0 + '@swc/helpers': 0.5.21 + '@tanstack/query-core': 5.100.14 + '@zxcvbn-ts/core': 3.0.4 + '@zxcvbn-ts/language-common': 3.0.4 + alien-signals: 2.0.6 + browser-tabs-lock: 1.3.0 + core-js: 3.47.0 + crypto-js: 4.2.0 + dequal: 2.0.3 + transitivePeerDependencies: + - react + - react-dom + '@clerk/clerk-js@6.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -11707,29 +11745,36 @@ snapshots: electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) - '@clerk/expo@4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3)': + '@clerk/expo@4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)': dependencies: - '@clerk/clerk-js': 6.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/react': 6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/clerk-js': 6.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/react': 6.12.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@expo/config-plugins': 56.0.9(typescript@6.0.3) base-64: 1.0.0 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) - react-native-url-polyfill: 4.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-url-polyfill: 4.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) tslib: 2.8.1 optionalDependencies: - expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: 56.0.4(expo@56.0.12) expo-secure-store: 56.0.4(expo@56.0.12) - expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) - react-dom: 19.2.6(react@19.2.6) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color - typescript + '@clerk/react@6.12.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + '@clerk/react@6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -11737,6 +11782,16 @@ snapshots: react-dom: 19.2.6(react@19.2.6) tslib: 2.8.1 + '@clerk/shared@4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/query-core': 5.100.14 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.7 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@clerk/shared@4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/query-core': 5.100.14 @@ -11924,6 +11979,12 @@ snapshots: '@drizzle-team/brocli@0.12.0': {} + '@effect/atom-react@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488))(react@19.2.3)(scheduler@0.27.0)': + dependencies: + effect: 4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488) + react: 19.2.3 + scheduler: 0.27.0 + '@effect/atom-react@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488))(react@19.2.6)(scheduler@0.27.0)': dependencies: effect: 4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488) @@ -12337,6 +12398,82 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.38': {} + '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': + dependencies: + '@expo/code-signing-certificates': 0.0.6 + '@expo/config': 56.0.9(typescript@6.0.3) + '@expo/config-plugins': 56.0.9(typescript@6.0.3) + '@expo/devcert': 1.2.1 + '@expo/env': 2.3.0 + '@expo/image-utils': 0.10.1(typescript@6.0.3) + '@expo/inline-modules': 0.0.12(typescript@6.0.3) + '@expo/json-file': 10.2.0 + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/metro-file-map': 56.0.3 + '@expo/osascript': 2.6.0 + '@expo/package-manager': 1.12.1 + '@expo/plist': 0.7.0 + '@expo/prebuild-config': 56.0.16(typescript@6.0.3) + '@expo/require-utils': 56.1.3(typescript@6.0.3) + '@expo/router-server': 56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@expo/schema-utils': 56.0.1 + '@expo/spawn-async': 1.8.0 + '@expo/ws-tunnel': 2.0.0(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@expo/xcpretty': 4.4.4 + '@react-native/dev-middleware': 0.85.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + accepts: 1.3.8 + arg: 5.0.2 + bplist-creator: 0.1.0 + bplist-parser: 0.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + compression: 1.8.1 + connect: 3.7.0 + debug: 4.4.3 + dnssd-advertise: 1.1.4 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-server: 56.0.5 + fetch-nodeshim: 0.4.10 + getenv: 2.0.0 + glob: 13.0.6 + lan-network: 0.2.1 + multitars: 1.0.0 + node-forge: 1.4.0 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 4.0.4 + pretty-format: 29.7.0 + progress: 2.0.3 + prompts: 2.4.2 + resolve-from: 5.0.0 + semver: 7.8.5 + send: 0.19.2 + slugify: 1.6.9 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + terminal-link: 2.1.1 + toqr: 0.1.1 + wrap-ansi: 7.0.0 + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + zod: 3.25.76 + optionalDependencies: + expo-router: 56.2.11(f859e3b6b8f7afda1532463233a5d60b) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - '@expo/dom-webview' + - '@expo/metro-runtime' + - bufferutil + - expo-constants + - expo-font + - react + - react-dom + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3)(utf-8-validate@6.0.6)': dependencies: '@expo/code-signing-certificates': 0.0.6 @@ -12412,6 +12549,7 @@ snapshots: - supports-color - typescript - utf-8-validate + optional: true '@expo/code-signing-certificates@0.0.6': dependencies: @@ -12461,18 +12599,33 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + chalk: 4.1.2 + optionalDependencies: + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': dependencies: chalk: 4.1.2 optionalDependencies: react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true + + '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': dependencies: expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true '@expo/env@2.3.0': dependencies: @@ -12546,6 +12699,15 @@ snapshots: - supports-color - typescript + '@expo/log-box@56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + anser: 1.4.10 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + stacktrace-parser: 0.1.11 + '@expo/log-box@56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': dependencies: '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) @@ -12554,6 +12716,7 @@ snapshots: react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) stacktrace-parser: 0.1.11 + optional: true '@expo/metro-config@56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6)': dependencies: @@ -12581,7 +12744,7 @@ snapshots: postcss: 8.5.15 resolve-from: 5.0.0 optionalDependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) transitivePeerDependencies: - bufferutil - supports-color @@ -12599,6 +12762,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/metro-runtime@56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + anser: 1.4.10 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + pretty-format: 29.7.0 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + '@expo/metro-runtime@56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) @@ -12611,6 +12787,7 @@ snapshots: whatwg-fetch: 3.6.20 optionalDependencies: react-dom: 19.2.6(react@19.2.6) + optional: true '@expo/metro@56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: @@ -12688,6 +12865,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/router-server@56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + debug: 4.4.3 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-server: 56.0.5 + react: 19.2.3 + optionalDependencies: + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-router: 56.2.11(f859e3b6b8f7afda1532463233a5d60b) + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - supports-color + '@expo/router-server@56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: debug: 4.4.3 @@ -12702,6 +12894,7 @@ snapshots: react-dom: 19.2.6(react@19.2.6) transitivePeerDependencies: - supports-color + optional: true '@expo/schema-utils@56.0.1': {} @@ -12728,6 +12921,23 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@types/react-dom' + optional: true + + '@expo/ui@56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0)': + dependencies: + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + sf-symbols-typescript: 2.2.0 + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + optionalDependencies: + '@babel/core': 7.29.7 + react-dom: 19.2.3(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' '@expo/ws-tunnel@2.0.0(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: @@ -13031,13 +13241,20 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.2.0(patch_hash=45e4cbcbeeca1b628b8480869374d7cbc77e3fbfa97ee0107a0d72c3c4a5d7f3)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': + '@legendapp/list@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: react: 19.2.6 use-sync-external-store: 1.6.0(react@19.2.6) optionalDependencies: react-dom: 19.2.6(react@19.2.6) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + + '@legendapp/list@3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@lexical/clipboard@0.41.0': dependencies: @@ -13637,16 +13854,16 @@ snapshots: tslib: 2.8.1 webcrypto-core: 1.9.2 - '@pierre/diffs@1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@pierre/diffs@1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@pierre/theme': 1.0.3 - '@pierre/theming': 0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0) + '@pierre/theming': 0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@4.2.0) '@shikijs/transformers': 4.2.0 diff: 8.0.3 hast-util-to-html: 9.0.5 lru_map: 0.4.1 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) shiki: 4.2.0 transitivePeerDependencies: - '@shikijs/themes' @@ -13667,12 +13884,12 @@ snapshots: '@pierre/theme@1.0.3': {} - '@pierre/theming@0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': + '@pierre/theming@0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@4.2.0)': optionalDependencies: '@pierre/theme': 1.0.3 '@shikijs/themes': 4.2.0 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) shiki: 4.2.0 '@pierre/theming@0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@4.3.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(shiki@4.2.0)': @@ -13683,6 +13900,13 @@ snapshots: react-dom: 19.2.6(react@19.2.6) shiki: 4.2.0 + '@pierre/trees@1.0.0-beta.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + preact: 11.0.0-beta.0 + preact-render-to-string: 6.6.5(preact@11.0.0-beta.0) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@pierre/trees@1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: preact: 11.0.0-beta.0 @@ -13696,6 +13920,19 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.6) @@ -13709,17 +13946,53 @@ snapshots: '@types/react-dom': 19.2.3(@types/react@19.2.16) optional: true + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.16)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-context@1.1.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 '@radix-ui/react-context@1.1.2(@types/react@19.2.16)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.16)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -13742,6 +14015,14 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + optional: true '@radix-ui/react-direction@1.1.1(@types/react@19.2.16)(react@19.2.6)': dependencies: @@ -13750,6 +14031,19 @@ snapshots: '@types/react': 19.2.16 optional: true + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -13762,12 +14056,31 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.16)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -13779,6 +14092,14 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + + '@radix-ui/react-id@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 '@radix-ui/react-id@1.1.1(@types/react@19.2.16)(react@19.2.6)': dependencies: @@ -13786,6 +14107,17 @@ snapshots: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -13796,6 +14128,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -13806,6 +14149,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -13815,12 +14168,31 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.6) '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.6) '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.6) '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.6) @@ -13834,12 +14206,28 @@ snapshots: '@types/react-dom': 19.2.3(@types/react@19.2.16) optional: true + '@radix-ui/react-slot@1.2.3(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + '@radix-ui/react-slot@1.2.3(@types/react@19.2.16)(react@19.2.6)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.6) react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + optional: true '@radix-ui/react-slot@1.2.4(@types/react@19.2.16)(react@19.2.6)': dependencies: @@ -13849,6 +14237,23 @@ snapshots: '@types/react': 19.2.16 optional: true + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -13866,11 +14271,26 @@ snapshots: '@types/react-dom': 19.2.3(@types/react@19.2.16) optional: true + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.16)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.16)(react@19.2.6)': dependencies: @@ -13879,6 +14299,14 @@ snapshots: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.16)(react@19.2.6)': dependencies: @@ -13886,6 +14314,14 @@ snapshots: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.16)(react@19.2.6)': dependencies: @@ -13893,12 +14329,20 @@ snapshots: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.16)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: '@types/react': 19.2.16 + optional: true '@react-grab/cli@0.1.44': dependencies: @@ -13911,16 +14355,22 @@ snapshots: prompts: 2.4.2 tinyexec: 1.2.4 + '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + optional: true + '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': dependencies: react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) optional: true - '@react-native-menu/menu@2.0.0(patch_hash=5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': + '@react-native-menu/menu@2.0.0(patch_hash=5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@react-native/assets-registry@0.85.3': {} @@ -14052,6 +14502,15 @@ snapshots: '@react-native/normalize-colors@0.85.3': {} + '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + optionalDependencies: + '@types/react': 19.2.16 + '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': dependencies: invariant: 2.2.4 @@ -14060,55 +14519,56 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) optionalDependencies: '@types/react': 19.2.16 + optional: true - '@react-navigation/core@7.21.2(react@19.2.6)': + '@react-navigation/core@7.21.2(react@19.2.3)': dependencies: '@react-navigation/routers': 7.6.0 escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.12 query-string: 7.1.3 - react: 19.2.6 + react: 19.2.3 react-is: 19.2.7 - use-latest-callback: 0.2.6(react@19.2.6) - use-sync-external-store: 1.6.0(react@19.2.6) + use-latest-callback: 0.2.6(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) - '@react-navigation/elements@2.9.26(773cc3c3d7b6f6f948921e6a125849bf)': + '@react-navigation/elements@2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4)': dependencies: - '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) color: 4.2.3 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) - react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) - use-latest-callback: 0.2.6(react@19.2.6) - use-sync-external-store: 1.6.0(react@19.2.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + use-latest-callback: 0.2.6(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: - '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@react-navigation/native-stack@7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(452cd56f59ca0a03ec356fcc4ca0055e)': + '@react-navigation/native-stack@7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(b7ff50e3b1e09b1815881c384829e9f8)': dependencies: - '@react-navigation/elements': 2.9.26(773cc3c3d7b6f6f948921e6a125849bf) - '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + '@react-navigation/elements': 2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4) + '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) color: 4.2.3 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) - react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) - react-native-screens: 4.25.2(patch_hash=282d02fa85a8a643548a27565dedd92bd3a729742be5c0371b0c4f6d07e33ce3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=282d02fa85a8a643548a27565dedd92bd3a729742be5c0371b0c4f6d07e33ce3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/native@7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)': + '@react-navigation/native@7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@react-navigation/core': 7.21.2(react@19.2.6) + '@react-navigation/core': 7.21.2(react@19.2.3) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.12 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) standard-navigation: 0.0.7 - use-latest-callback: 0.2.6(react@19.2.6) + use-latest-callback: 0.2.6(react@19.2.3) '@react-navigation/routers@7.6.0': dependencies: @@ -14452,25 +14912,25 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(2a2e612476bcb1a5250beb3b8fa0a80c)': + '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe)': dependencies: - expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3) - expo-clipboard: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-clipboard: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-haptics: 56.0.3(expo@56.0.12) - expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) - react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@t3tools/mobile-review-diff-native@file:apps/mobile/modules/t3-review-diff': {} '@t3tools/mobile-terminal-native@file:apps/mobile/modules/t3-terminal': {} - '@tabler/icons-react-native@3.44.0(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react@19.2.6)': + '@tabler/icons-react-native@3.44.0(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react@19.2.3)': dependencies: '@tabler/icons': 3.44.0 - react: 19.2.6 - react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + react: 19.2.3 + react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@tabler/icons@3.44.0': {} @@ -15609,8 +16069,8 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - expo-widgets: 56.0.19(32843e0c0883df8bccfa0b8323659df5) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -15662,8 +16122,8 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - expo-widgets: 56.0.19(32843e0c0883df8bccfa0b8323659df5) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -16553,7 +17013,18 @@ snapshots: expo-application@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + + expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): + dependencies: + '@expo/image-utils': 0.10.1(typescript@6.0.3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - supports-color + - typescript expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3): dependencies: @@ -16565,48 +17036,57 @@ snapshots: transitivePeerDependencies: - supports-color - typescript + optional: true - expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: expo-application: 56.0.3(expo@56.0.12) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: 56.0.4(expo@56.0.12) - expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) - expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - expo - supports-color - expo-blur@56.0.3(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-blur@56.0.3(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-build-properties@56.0.19(expo@56.0.12): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) resolve-from: 5.0.0 semver: 7.8.5 - expo-camera@56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-camera@56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: barcode-detector: 3.2.0(@types/emscripten@1.41.5) - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@types/emscripten' - expo-clipboard@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-clipboard@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + + expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + dependencies: + '@expo/env': 2.3.0 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - supports-color expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): dependencies: @@ -16615,46 +17095,60 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color + optional: true expo-crypto@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): + expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) - expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-dev-menu-interface: 56.0.1(expo@56.0.12) expo-manifests: 56.0.4(expo@56.0.12) expo-updates-interface: 56.0.2(expo@56.0.12) transitivePeerDependencies: - react-native - expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): + expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-manifests: 56.0.4(expo@56.0.12) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-dev-menu-interface@56.0.1(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): + expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-dev-menu-interface: 56.0.1(expo@56.0.12) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-eas-client@56.0.1: {} + expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + dependencies: + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): dependencies: expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true + + expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + fontfaceobserver: 2.3.0 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: @@ -16662,39 +17156,63 @@ snapshots: fontfaceobserver: 2.3.0 react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true + + expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true expo-haptics@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-image-loader@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-image-picker@56.0.18(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-image-loader: 56.0.3(expo@56.0.12) - expo-image@56.0.11(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-image@56.0.11(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 expo-json-utils@56.0.0: {} + expo-keep-awake@56.0.3(expo@56.0.12)(react@19.2.3): + dependencies: + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + expo-keep-awake@56.0.3(expo@56.0.12)(react@19.2.6): dependencies: expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) react: 19.2.6 + optional: true + + expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - expo + - supports-color expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: @@ -16705,10 +17223,11 @@ snapshots: transitivePeerDependencies: - expo - supports-color + optional: true expo-manifests@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-json-utils: 56.0.0 expo-modules-autolinking@56.0.16(typescript@6.0.3): @@ -16721,6 +17240,16 @@ snapshots: - supports-color - typescript + expo-modules-core@56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + '@expo/expo-modules-macros-plugin': 0.2.2 + expo-modules-jsi: 56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + optionalDependencies: + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-modules-core@56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: '@expo/expo-modules-macros-plugin': 0.2.2 @@ -16730,40 +17259,46 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) optionalDependencies: react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + optional: true + + expo-modules-jsi@56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + dependencies: + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-modules-jsi@56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): dependencies: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true - expo-network@56.0.5(expo@56.0.12)(react@19.2.6): + expo-network@56.0.5(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 - expo-notifications@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3): + expo-notifications@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) abort-controller: 3.0.0 badgin: 1.2.3 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-application: 56.0.3(expo@56.0.12) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript - expo-paste-input@0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-paste-input@0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-quick-actions@6.0.2(expo@56.0.12)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.8.14(typescript@6.0.3) - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) schema-utils: 4.3.3 sf-symbols-typescript: 2.2.0 transitivePeerDependencies: @@ -16821,20 +17356,71 @@ snapshots: - supports-color optional: true + expo-router@56.2.11(f859e3b6b8f7afda1532463233a5d60b): + dependencies: + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/schema-utils': 56.0.1 + '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + client-only: 0.0.1 + color: 4.2.3 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-glass-effect: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-server: 56.0.5 + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.12 + query-string: 7.1.3 + react: 19.2.3 + react-fast-compare: 3.2.2 + react-is: 19.2.7 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-drawer-layout: 4.2.4(05364bd849de538917a7364cc7dee3f5) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=282d02fa85a8a643548a27565dedd92bd3a729742be5c0371b0c4f6d07e33ce3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + server-only: 0.0.1 + sf-symbols-typescript: 2.2.0 + shallowequal: 1.1.0 + standard-navigation: 0.0.5 + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + react-native-gesture-handler: 2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + transitivePeerDependencies: + - '@babel/core' + - '@testing-library/dom' + - '@types/react' + - '@types/react-dom' + - expo-font + - react-native-worklets + - supports-color + optional: true + expo-secure-store@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-server@56.0.5: {} - expo-sharing@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(typescript@6.0.3): + expo-sharing@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/config-plugins': 56.0.9(typescript@6.0.3) '@expo/config-types': 56.0.6 '@expo/plist': 0.7.0 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript @@ -16843,21 +17429,38 @@ snapshots: dependencies: '@expo/config-plugins': 56.0.9(typescript@6.0.3) '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) xml2js: 0.6.0 transitivePeerDependencies: - supports-color - typescript + expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + await-lock: 2.2.2 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: await-lock: 2.2.2 expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true expo-structured-headers@56.0.0: {} + expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + '@expo-google-fonts/material-symbols': 0.4.38 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + sf-symbols-typescript: 2.2.0 + expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: '@expo-google-fonts/material-symbols': 0.4.38 @@ -16866,12 +17469,13 @@ snapshots: react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 + optional: true expo-updates-interface@56.0.2(expo@56.0.12): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-updates@56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + expo-updates@56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.7.0 @@ -16879,7 +17483,7 @@ snapshots: arg: 4.1.3 chalk: 4.1.2 debug: 4.4.3 - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-eas-client: 56.0.1 expo-manifests: 56.0.4(expo@56.0.12) expo-structured-headers: 56.0.0 @@ -16888,26 +17492,26 @@ snapshots: glob: 13.0.6 ignore: 5.3.2 nullthrows: 1.1.1 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) resolve-from: 5.0.0 optionalDependencies: - expo-dev-client: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)) + expo-dev-client: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) transitivePeerDependencies: - supports-color - expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): + expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@56.0.19(32843e0c0883df8bccfa0b8323659df5): + expo-widgets@56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0): dependencies: '@expo/plist': 0.7.0 - '@expo/ui': 56.0.18(32843e0c0883df8bccfa0b8323659df5) - expo: 56.0.12(deb8cbf3e0f411b34ba85a995c9982ba) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@babel/core' - '@types/react' @@ -16916,6 +17520,48 @@ snapshots: - react-native-reanimated - react-native-worklets + expo@56.0.12(8895228379997a2a064f9644cda56ed0): + dependencies: + '@babel/runtime': 7.29.7 + '@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/config': 56.0.9(typescript@6.0.3) + '@expo/config-plugins': 56.0.9(typescript@6.0.3) + '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/fingerprint': 0.19.4 + '@expo/local-build-cache-provider': 56.0.8(typescript@6.0.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@ungap/structured-clone': 1.3.1 + babel-preset-expo: 56.0.15(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.19)(expo@56.0.12)(react-refresh@0.14.2) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-file-system: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-keep-awake: 56.0.3(expo@56.0.12)(react@19.2.3) + expo-modules-autolinking: 56.0.16(typescript@6.0.3) + expo-modules-core: 56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + pretty-format: 29.7.0 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-refresh: 0.14.2 + whatwg-url-minimum: 0.1.2 + optionalDependencies: + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-dom: 19.2.3(react@19.2.3) + react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - expo-router + - expo-widgets + - react-native-worklets + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + expo@56.0.12(deb8cbf3e0f411b34ba85a995c9982ba): dependencies: '@babel/runtime': 7.29.7 @@ -16957,6 +17603,7 @@ snapshots: - supports-color - typescript - utf-8-validate + optional: true exponential-backoff@3.1.3: {} @@ -19397,6 +20044,11 @@ snapshots: - bufferutil - utf-8-validate + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 @@ -19409,9 +20061,14 @@ snapshots: react-fast-compare@3.2.2: optional: true + react-freeze@1.0.4(react@19.2.3): + dependencies: + react: 19.2.3 + react-freeze@1.0.4(react@19.2.6): dependencies: react: 19.2.6 + optional: true react-grab@0.1.44(react@19.2.6): dependencies: @@ -19446,6 +20103,16 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-drawer-layout@4.2.4(05364bd849de538917a7364cc7dee3f5): + dependencies: + color: 4.2.3 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-gesture-handler: 2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + use-latest-callback: 0.2.6(react@19.2.3) + optional: true + react-native-drawer-layout@4.2.4(de9b2f2dc96a3557fdc0df187a8417ee): dependencies: color: 4.2.3 @@ -19456,6 +20123,15 @@ snapshots: use-latest-callback: 0.2.6(react@19.2.6) optional: true + react-native-gesture-handler@2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + '@egjs/hammerjs': 2.0.17 + '@types/react-test-renderer': 19.1.0 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-gesture-handler@2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: '@egjs/hammerjs': 2.0.17 @@ -19464,36 +20140,51 @@ snapshots: invariant: 2.2.4 react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true - react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + + react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true - react-native-keyboard-controller@1.21.13(patch_hash=20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008)(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + react-native-keyboard-controller@1.21.13(patch_hash=20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008)(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) - react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) optionalDependencies: - react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) + react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + + react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + semver: 7.8.5 react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: @@ -19502,11 +20193,25 @@ snapshots: react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) semver: 7.8.5 + optional: true + + react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true + + react-native-screens@4.25.2(patch_hash=282d02fa85a8a643548a27565dedd92bd3a729742be5c0371b0c4f6d07e33ce3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + react: 19.2.3 + react-freeze: 1.0.4(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + warn-once: 0.1.1 react-native-screens@4.25.2(patch_hash=282d02fa85a8a643548a27565dedd92bd3a729742be5c0371b0c4f6d07e33ce3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: @@ -19514,25 +20219,33 @@ snapshots: react-freeze: 1.0.4(react@19.2.6) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) warn-once: 0.1.1 + optional: true - react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): + react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: css-select: 5.2.2 css-tree: 1.1.3 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) warn-once: 0.1.1 - react-native-url-polyfill@4.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6)): + react-native-url-polyfill@4.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + + react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: @@ -19540,6 +20253,27 @@ snapshots: invariant: 2.2.4 react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + optional: true + + react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + convert-source-map: 2.0.0 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + semver: 7.8.5 + transitivePeerDependencies: + - supports-color react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6): dependencies: @@ -19560,6 +20294,52 @@ snapshots: semver: 7.8.5 transitivePeerDependencies: - supports-color + optional: true + + react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6): + dependencies: + '@react-native/assets-registry': 0.85.3 + '@react-native/codegen': 0.85.3(@babel/core@7.29.7) + '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@react-native/gradle-plugin': 0.85.3 + '@react-native/js-polyfills': 0.85.3 + '@react-native/normalize-colors': 0.85.3 + '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-plugin-syntax-hermes-parser: 0.33.3 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + hermes-compiler: 250829098.0.10 + invariant: 2.2.4 + memoize-one: 5.2.1 + metro-runtime: 0.84.4 + metro-source-map: 0.84.4 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.3 + react-devtools-core: 6.1.5(bufferutil@4.1.0)(utf-8-validate@6.0.6) + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 + semver: 7.8.5 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.17 + whatwg-fetch: 3.6.20 + ws: 7.5.11(bufferutil@4.1.0)(utf-8-validate@6.0.6) + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.2.16 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6): dependencies: @@ -19605,6 +20385,7 @@ snapshots: - bufferutil - supports-color - utf-8-validate + optional: true react-reconciler@0.33.0(react@19.2.6): dependencies: @@ -19613,6 +20394,14 @@ snapshots: react-refresh@0.14.2: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.16)(react@19.2.3): + dependencies: + react: 19.2.3 + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + react-remove-scroll-bar@2.3.8(@types/react@19.2.16)(react@19.2.6): dependencies: react: 19.2.6 @@ -19620,6 +20409,18 @@ snapshots: tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.16 + optional: true + + react-remove-scroll@2.7.2(@types/react@19.2.16)(react@19.2.3): + dependencies: + react: 19.2.3 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.16)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.3) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.16)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.16)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 react-remove-scroll@2.7.2(@types/react@19.2.16)(react@19.2.6): dependencies: @@ -19631,6 +20432,15 @@ snapshots: use-sidecar: 1.1.3(@types/react@19.2.16)(react@19.2.6) optionalDependencies: '@types/react': 19.2.16 + optional: true + + react-style-singleton@2.2.3(@types/react@19.2.16)(react@19.2.3): + dependencies: + get-nonce: 1.0.1 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 react-style-singleton@2.2.3(@types/react@19.2.16)(react@19.2.6): dependencies: @@ -19639,6 +20449,9 @@ snapshots: tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.16 + optional: true + + react@19.2.3: {} react@19.2.6: {} @@ -20659,14 +21472,14 @@ snapshots: universalify@2.0.1: {} - uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6)(tailwindcss@4.3.0): + uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0): dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 culori: 4.0.2 lightningcss: 1.30.1 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) tailwindcss: 4.3.0 unpipe@1.0.0: {} @@ -20718,16 +21531,37 @@ snapshots: punycode: 2.3.1 optional: true + use-callback-ref@1.3.3(@types/react@19.2.16)(react@19.2.3): + dependencies: + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + use-callback-ref@1.3.3(@types/react@19.2.16)(react@19.2.6): dependencies: react: 19.2.6 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.16 + optional: true + + use-latest-callback@0.2.6(react@19.2.3): + dependencies: + react: 19.2.3 use-latest-callback@0.2.6(react@19.2.6): dependencies: react: 19.2.6 + optional: true + + use-sidecar@1.1.3(@types/react@19.2.16)(react@19.2.3): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 use-sidecar@1.1.3(@types/react@19.2.16)(react@19.2.6): dependencies: @@ -20736,6 +21570,11 @@ snapshots: tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.16 + optional: true + + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 use-sync-external-store@1.6.0(react@19.2.6): dependencies: @@ -20762,6 +21601,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -20770,6 +21618,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@types/react-dom' + optional: true vfile-location@5.0.3: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 42529f56ee3..c254405aa16 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -127,7 +127,7 @@ patchedDependencies: "@effect/vitest@4.0.0-beta.102": patches/@effect__vitest@4.0.0-beta.102.patch "@expo/metro-config@56.0.14": patches/@expo%2Fmetro-config@56.0.14.patch "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch - "@legendapp/list@3.2.0": patches/@legendapp__list@3.2.0.patch + "@legendapp/list@3.3.3": patches/@legendapp__list@3.3.3.patch "@pierre/diffs@1.3.0-beta.5": patches/@pierre%2Fdiffs@1.3.0-beta.5.patch "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch diff --git a/scripts/mobile-showcase-environment.ts b/scripts/mobile-showcase-environment.ts index 5b4c22a8783..28ce513cf92 100644 --- a/scripts/mobile-showcase-environment.ts +++ b/scripts/mobile-showcase-environment.ts @@ -216,16 +216,42 @@ export const SHOWCASE_THREADS = [ response: "The plan groups milestones without changing the underlying log stream, preserves plain-text output, and adds zero work to the hot path.", }, + // Finished work, settled by hand: the list keeps it as a receded tail so + // the active block above reads as everything still in flight. The active + // block stays small enough that the settled tail begins above the fold — + // a store screenshot has to show that history exists, not just imply it. { - id: "scheduler-breathe", + id: "handoff-haptics", + projectId: "t3code", + title: "Tune the handoff haptics", + branch: "feat/handoff-haptics", + minutesAgo: 5 * 60, + settled: true, + request: "Give the desktop-to-phone handoff a haptic that lands with the animation.", + response: + "The handoff now taps once as the thread lands and stays silent on failure, so the phone never celebrates a handoff that did not happen.", + }, + { + id: "streaming-shell", + projectId: "react", + title: "Stream the shell before the data", + branch: "feat/streaming-shell", + minutesAgo: 28 * 60, + settled: true, + request: "Get the app shell painted before any data request resolves.", + response: + "The shell now flushes on first byte and the data boundaries hydrate underneath it, so the first paint no longer waits on the slowest query.", + }, + { + id: "quieter-oom", projectId: "linux", - title: "Let the scheduler breathe", - branch: "perf/scheduler-breathe", - minutesAgo: 76, - request: - "Find a calmer balancing strategy for bursty mixed workloads without hurting tail latency.", + title: "Make the OOM killer explain itself", + branch: "feat/quieter-oom", + minutesAgo: 2 * 24 * 60, + settled: true, + request: "Make out-of-memory kills legible without adding a single allocation to the hot path.", response: - "The new heuristic reduces needless migrations during short bursts while preserving the existing latency guardrails.", + "Kills now report the winning heuristic and the runner-up alongside the usual dump, assembled entirely from data the path already had.", }, ] as const; @@ -315,6 +341,7 @@ function insertThread( readonly branch: string; readonly minutesAgo: number; readonly state?: "working" | "approval" | "plan"; + readonly settled?: boolean; readonly workspaceRoot: string; }, ): void { @@ -327,8 +354,8 @@ function insertThread( thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, branch, worktree_path, latest_turn_id, latest_user_message_at, pending_approval_count, pending_user_input_count, has_actionable_proposed_plan, created_at, updated_at, - archived_at, deleted_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL)`, + archived_at, deleted_at, settled_override, settled_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL, ?, ?)`, ) .run( input.id, @@ -345,6 +372,8 @@ function insertThread( input.state === "plan" ? 1 : 0, minutesBefore(now, input.minutesAgo + 120), updatedAt, + input.settled ? "settled" : null, + input.settled ? updatedAt : null, ); database .prepare( @@ -380,7 +409,11 @@ function seedDatabase( threads: ReadonlyArray<(typeof SHOWCASE_THREADS)[number]>, now: number, ): void { - const database = new NodeSqlite.DatabaseSync(dbPath); + // The environment server is already running against this file and keeps + // writing (migrations, projections) while we seed, so the write lock is + // genuinely contended — without a busy timeout `BEGIN IMMEDIATE` fails + // instantly with SQLITE_BUSY on a loaded machine. + const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 }); try { database.exec("BEGIN IMMEDIATE"); for (const table of [ @@ -521,7 +554,14 @@ function seedDatabase( } database.exec("COMMIT"); } catch (error) { - database.exec("ROLLBACK"); + // A failed BEGIN (or an error SQLite already auto-rolled back) leaves no + // transaction, and the rollback's own "cannot rollback" error would then + // replace the one that actually explains the failure. + try { + database.exec("ROLLBACK"); + } catch { + // Nothing to roll back. + } throw error; } finally { database.close(); diff --git a/scripts/mobile-showcase.test.ts b/scripts/mobile-showcase.test.ts index 0e5373c8f55..f0c5d02f213 100644 --- a/scripts/mobile-showcase.test.ts +++ b/scripts/mobile-showcase.test.ts @@ -293,8 +293,23 @@ it("seeds a playful multi-environment project spectrum", () => { SHOWCASE_ENVIRONMENTS.map((environment) => environment.label), ["Moonbase Terminal", "Suspense Station", "Kernel Cabin"], ); - assert.equal(SHOWCASE_THREADS.length, 6); + assert.equal(SHOWCASE_THREADS.length, 8); assert.equal(new Set(SHOWCASE_THREADS.map((thread) => thread.projectId)).size, 3); + // Every project contributes to both the active block and the settled tail, + // so each list scope screenshots with the same two-part structure. + for (const project of SHOWCASE_PROJECTS) { + const projectThreads = SHOWCASE_THREADS.filter((thread) => thread.projectId === project.id); + assert.equal( + projectThreads.some((thread) => "settled" in thread && thread.settled), + true, + `${project.title} has no settled thread`, + ); + assert.equal( + projectThreads.some((thread) => !("settled" in thread && thread.settled)), + true, + `${project.title} has no active thread`, + ); + } assert.equal( SHOWCASE_PROJECTS.every((project) => project.favicon.includes(" { if (import.meta.main) { void main().catch((error: unknown) => { - NodeProcess.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + // Stack over message: the harness only fails in CI, where the line that + // threw is the whole diagnosis and there is nobody at a terminal to + // re-run it with more output. + NodeProcess.stderr.write( + `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`, + ); NodeProcess.exit(1); }); }