From a29a7cc5853e16d4ca7e07acf364836f71b6101a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 9 Sep 2026 10:47:15 -0700 Subject: [PATCH 1/2] fix(mobile): blur glass fallbacks to prevent background text bleed (#10964) --- apps/mobile/src/components/GlassSurface.tsx | 43 +++++++-- .../terminal/ThreadTerminalRouteScreen.tsx | 22 ++++- .../src/features/threads/ThreadComposer.tsx | 10 +- .../features/threads/ThreadDetailScreen.tsx | 92 +++++++++++-------- apps/mobile/src/lib/glassBlurTarget.ts | 6 ++ 5 files changed, 119 insertions(+), 54 deletions(-) create mode 100644 apps/mobile/src/lib/glassBlurTarget.ts diff --git a/apps/mobile/src/components/GlassSurface.tsx b/apps/mobile/src/components/GlassSurface.tsx index 577c43aa890a..390863460df3 100644 --- a/apps/mobile/src/components/GlassSurface.tsx +++ b/apps/mobile/src/components/GlassSurface.tsx @@ -1,17 +1,20 @@ +import { BlurView } from "expo-blur"; import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect"; -import type { ReactNode, Ref } from "react"; +import { useContext, type ReactNode, type Ref, type RefObject } from "react"; import { Platform, + StyleSheet, useColorScheme, View, type ColorValue, - type StyleProp, type ViewProps, type ViewStyle, } from "react-native"; import { withUniwind } from "uniwind"; import { cn } from "../lib/cn"; +import { GlassBlurTargetContext } from "../lib/glassBlurTarget"; +import { themeColorWithAlpha } from "../lib/mobileTheme"; // Explicit mappings keep the native glassEffectStyle enum out of style-array conversion. const ThemedGlassView = withUniwind(GlassView, { @@ -26,8 +29,9 @@ interface GlassSurfaceProps extends ViewProps { readonly tintColor?: ColorValue; readonly tintColorClassName?: string; readonly chrome?: "default" | "none"; - /** Styling used only when native Liquid Glass is unavailable. */ - readonly fallbackStyle?: StyleProp; + /** Base color for the frosted tint, or solid fill when blur is unavailable. */ + readonly fallbackColor?: ColorValue; + readonly blurTarget?: RefObject; /** Uniwind styling used only when native Liquid Glass is unavailable. */ readonly fallbackClassName?: string; } @@ -39,13 +43,21 @@ export function GlassSurface({ chrome = "default", tintColor, tintColorClassName, - fallbackStyle, + fallbackColor, + blurTarget, fallbackClassName, className, style, ...props }: GlassSurfaceProps) { const isDarkMode = useColorScheme() === "dark"; + const inheritedBlurTarget = useContext(GlassBlurTargetContext); + const target = blurTarget ?? inheritedBlurTarget; + const supportsBlur = + Platform.OS === "ios" || + (Platform.OS === "android" && Platform.Version >= 31 && target !== undefined); + const backgroundColor = + fallbackColor === undefined ? undefined : themeColorWithAlpha(String(fallbackColor), 1); const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable(); const surfaceStyle: ViewStyle = { borderRadius: 32, @@ -95,14 +107,27 @@ export function GlassSurface({ {...props} ref={ref} className={cn( - chrome === "none" - ? "border-0 border-transparent bg-transparent" - : "border border-border bg-glass-surface", + chrome === "none" ? "border-0 border-transparent" : "border border-border", fallbackClassName, className, )} - style={[surfaceStyle, fallbackStyle, style]} + style={[surfaceStyle, style]} > + {supportsBlur ? ( + + ) : null} + {children} ); diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index bca54694bd7b..bc2e6fc3c64a 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -1,3 +1,4 @@ +import { BlurTargetView } from "expo-blur"; import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal"; import type { MenuAction } from "@react-native-menu/menu"; @@ -154,6 +155,7 @@ type ThreadTerminalRouteScreenProps = StaticScreenProps<{ }>; export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) { + const terminalBlurTarget = useRef(null); const navigation = useNavigation(); const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); const resizeTerminal = useAtomCommand(terminalEnvironment.resize, "terminal resize"); @@ -1260,7 +1262,21 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) /> ) : ( <> - + + - + {isAccessoryVisible ? ( { @@ -839,17 +841,27 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const handleFeedTouchCancel = useCallback(() => { feedTouchStartRef.current = null; }, []); + const feedBlurTarget = useRef(null); return ( {showContent ? ( - + - + ) : ( )} @@ -1003,41 +1015,43 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread : undefined } > - + + + diff --git a/apps/mobile/src/lib/glassBlurTarget.ts b/apps/mobile/src/lib/glassBlurTarget.ts new file mode 100644 index 000000000000..4e6f7d3e3e88 --- /dev/null +++ b/apps/mobile/src/lib/glassBlurTarget.ts @@ -0,0 +1,6 @@ +import { createContext, type RefObject } from "react"; +import type { View } from "react-native"; + +// Android cannot sample a target that contains the BlurView itself. Keep the +// feed in a separate target, shared by the composer and its popovers. +export const GlassBlurTargetContext = createContext | undefined>(undefined); From e16b8b059c9f5ff6dfed1addecffb831c6aee043 Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:36:53 +0530 Subject: [PATCH 2/2] feat(web): add provider model bulk toggle (#10947) --- .../settings/ProviderModelsSection.test.ts | 19 +++++++- .../settings/ProviderModelsSection.tsx | 43 ++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/settings/ProviderModelsSection.test.ts b/apps/web/src/components/settings/ProviderModelsSection.test.ts index 83adbeb97320..cbb95472da19 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.test.ts +++ b/apps/web/src/components/settings/ProviderModelsSection.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vite-plus/test"; import type { ServerProviderModel } from "@t3tools/contracts"; -import { groupModelsForDisplay } from "./ProviderModelsSection"; +import { groupModelsForDisplay, nextHiddenModelsForBulkToggle } from "./ProviderModelsSection"; function model(slug: string, isCustom = false): ServerProviderModel { return { slug, name: slug, isCustom, capabilities: null }; @@ -21,3 +21,20 @@ describe("groupModelsForDisplay", () => { expect(display.map((entry) => entry.slug)).toEqual(["c", "d", "b", "custom", "a"]); }); }); + +describe("nextHiddenModelsForBulkToggle", () => { + it("hides every built-in model without hiding custom models", () => { + const models = [model("a"), model("b"), model("custom", true)]; + + expect(nextHiddenModelsForBulkToggle(models, ["a"])).toEqual(["a", "b"]); + }); + + it("shows every built-in model while preserving unrelated hidden entries", () => { + const models = [model("a"), model("b"), model("custom", true)]; + + expect(nextHiddenModelsForBulkToggle(models, ["a", "b", "legacy", "custom"])).toEqual([ + "legacy", + "custom", + ]); + }); +}); diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx index 7866578cfa4a..505a86ff0eac 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.tsx +++ b/apps/web/src/components/settings/ProviderModelsSection.tsx @@ -96,6 +96,21 @@ export function groupModelsForDisplay< ]; } +export function nextHiddenModelsForBulkToggle( + models: ReadonlyArray>, + hiddenModels: ReadonlyArray, +): string[] { + const builtInSlugs = models.filter((model) => !model.isCustom).map((model) => model.slug); + const builtInSlugSet = new Set(builtInSlugs); + const allBuiltInModelsHidden = builtInSlugs.every((slug) => hiddenModels.includes(slug)); + + if (allBuiltInModelsHidden) { + return hiddenModels.filter((slug) => !builtInSlugSet.has(slug)); + } + + return [...new Set([...hiddenModels, ...builtInSlugs])]; +} + interface ProviderModelsSectionProps { /** Identifier used to namespace input ids within the DOM. */ readonly instanceId: ProviderInstanceId; @@ -181,6 +196,8 @@ export function ProviderModelsSection({ (model) => !model.isCustom && hiddenModelSet.has(model.slug), ).length; const builtInModels = useMemo(() => models.filter((model) => !model.isCustom), [models]); + const allBuiltInModelsHidden = + builtInModels.length > 0 && builtInModels.every((model) => hiddenModelSet.has(model.slug)); const showFilter = models.length > FILTER_THRESHOLD; const normalizedFilter = filter.trim().toLowerCase(); const isFiltering = showFilter && normalizedFilter.length > 0; @@ -502,11 +519,27 @@ export function ProviderModelsSection({ aria-label="Filter models" /> ) : null} - - {models.length} model{models.length === 1 ? "" : "s"} - {favoriteCount > 0 ? ` · ${favoriteCount} favorite${favoriteCount === 1 ? "" : "s"}` : ""} - {hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""} - +
+ {builtInModels.length > 0 ? ( + + ) : null} + + {models.length} model{models.length === 1 ? "" : "s"} + {favoriteCount > 0 + ? ` · ${favoriteCount} favorite${favoriteCount === 1 ? "" : "s"}` + : ""} + {hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""} + +