authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-17 14:03:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 14:03:01-07:00
log6deac7a924e6d90eef400cd218703cd3ad00a383
tree2034f86b5633d1df203dd05026775621231548d3
parent675869d2b0f9e059d5950b80390c0cad1cb1590e
signature Signed by PGP key B5690EEEBB952194

fix(mobile): show Agent behavior icon on Android (#12316)


4 files changed, 48 insertions(+), 20 deletions(-)

apps/mobile/src/components/AppSymbol.ios.tsx+4-3
......@@ -1,14 +1,15 @@
1import { SymbolView as ExpoSymbolView, type SymbolViewProps } from "expo-symbols";
1import { SymbolView as ExpoSymbolView } from "expo-symbols";
22import { withUniwind } from "uniwind";
3import type { AppSymbolViewProps } from "./AppSymbol";
34
45export type { SFSymbol } from "expo-symbols";
5export type AppSymbolName = SymbolViewProps["name"];
6export type { AppSymbolName } from "./AppSymbol";
67
78/**
89 * Keep the iOS implementation isolated from the Android Tabler fallback so
910 * Metro does not initialize the icon package when iOS renders SF Symbols.
1011 */
11function AppSymbolView(props: SymbolViewProps) {
12function AppSymbolView(props: AppSymbolViewProps) {
1213 return <ExpoSymbolView {...props} />;
1314}
1415
apps/mobile/src/components/AppSymbol.tsx+39-10
......@@ -4,11 +4,14 @@ import type { Icon } from "@tabler/icons-react-native/types";
44 * the entire Tabler icon set in Metro.
55 */
66import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal";
7import IconAlignLeft from "@tabler/icons-react-native/IconAlignLeft";
78import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle";
89import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle";
910import IconApps from "@tabler/icons-react-native/IconApps";
1011import IconArchive from "@tabler/icons-react-native/IconArchive";
1112import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp";
13import IconArrowDown from "@tabler/icons-react-native/IconArrowDown";
14import IconArrowForwardUp from "@tabler/icons-react-native/IconArrowForwardUp";
1215import IconArrowLeft from "@tabler/icons-react-native/IconArrowLeft";
1316import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle";
1417import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle";
......@@ -30,15 +33,17 @@ import IconChevronDown from "@tabler/icons-react-native/IconChevronDown";
3033import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft";
3134import IconChevronRight from "@tabler/icons-react-native/IconChevronRight";
3235import IconChevronUp from "@tabler/icons-react-native/IconChevronUp";
36import IconCircle from "@tabler/icons-react-native/IconCircle";
3337import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck";
3438import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled";
3539import IconTicket from "@tabler/icons-react-native/IconTicket";
3640import IconClock from "@tabler/icons-react-native/IconClock";
3741import IconCode from "@tabler/icons-react-native/IconCode";
3842import IconCopy from "@tabler/icons-react-native/IconCopy";
39import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop";
4043import IconDatabase from "@tabler/icons-react-native/IconDatabase";
44import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop";
4145import IconDeviceLaptop from "@tabler/icons-react-native/IconDeviceLaptop";
46import IconDeviceMobile from "@tabler/icons-react-native/IconDeviceMobile";
4247import IconDots from "@tabler/icons-react-native/IconDots";
4348import IconDotsVertical from "@tabler/icons-react-native/IconDotsVertical";
4449import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal";
......@@ -62,13 +67,15 @@ import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns";
6267import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar";
6368import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight";
6469import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing";
65import IconMenu2 from "@tabler/icons-react-native/IconMenu2";
70import IconMicrophone from "@tabler/icons-react-native/IconMicrophone";
6671import IconLink from "@tabler/icons-react-native/IconLink";
72import IconMenu2 from "@tabler/icons-react-native/IconMenu2";
6773import IconMessage from "@tabler/icons-react-native/IconMessage";
6874import IconMinus from "@tabler/icons-react-native/IconMinus";
6975import IconMoon from "@tabler/icons-react-native/IconMoon";
7076import IconNetwork from "@tabler/icons-react-native/IconNetwork";
7177import IconPalette from "@tabler/icons-react-native/IconPalette";
78import IconPencil from "@tabler/icons-react-native/IconPencil";
7279import IconPhoto from "@tabler/icons-react-native/IconPhoto";
7380import IconPin from "@tabler/icons-react-native/IconPin";
7481import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff";
......@@ -97,13 +104,14 @@ import IconUserCircle from "@tabler/icons-react-native/IconUserCircle";
97104import IconWifiOff from "@tabler/icons-react-native/IconWifiOff";
98105import IconWorld from "@tabler/icons-react-native/IconWorld";
99106import IconX from "@tabler/icons-react-native/IconX";
100import type { SFSymbol, SymbolViewProps } from "expo-symbols";
107import type { AndroidSymbol, SFSymbol, SymbolViewProps } from "expo-symbols";
101108import { withUniwind } from "uniwind";
102109
103const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
110const ANDROID_ICON_BY_SF_SYMBOL = {
104111 "arrow.branch": IconGitBranch,
105112 "arrow.left": IconArrowLeft,
106113 "arrow.clockwise": IconRefresh,
114 "arrow.down": IconArrowDown,
107115 "arrow.down.circle": IconArrowDownCircle,
108116 "arrow.right.circle": IconArrowRightCircle,
109117 "arrow.triangle.branch": IconGitBranch,
......@@ -117,6 +125,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
117125 "arrow.up.right": IconArrowUpRight,
118126 "arrow.up.right.circle": IconArrowUpRightCircle,
119127 "arrow.uturn.backward": IconArrowBackUp,
128 "arrow.uturn.forward": IconArrowForwardUp,
120129 archivebox: IconArchive,
121130 "archivebox.fill": IconArchive,
122131 "bell.badge": IconBellRinging,
......@@ -127,6 +136,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
127136 "chart.bar.xaxis": IconChartBar,
128137 checkmark: IconCheck,
129138 "checkmark.circle": IconCircleCheck,
139 circle: IconCircle,
130140 clock: IconClock,
131141 ticket: IconTicket,
132142 cloud: IconCloud,
......@@ -143,11 +153,13 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
143153 moon: IconMoon,
144154 "ellipsis.circle": IconDotsCircleHorizontal,
145155 "exclamationmark.triangle": IconAlertTriangle,
156 "exclamationmark.circle": IconAlertCircle,
146157 eye: IconEye,
147158 folder: IconFolder,
148159 "folder.badge.plus": IconFolderPlus,
149160 "folder.fill": IconFolder,
150161 gearshape: IconSettings,
162 globe: IconWorld,
151163 hammer: IconHammer,
152164 "info.circle": IconInfoCircle,
153165 internaldrive: IconDatabase,
......@@ -162,7 +174,9 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
162174 macmini: IconServer,
163175 macstudio: IconDeviceDesktop,
164176 magnifyingglass: IconSearch,
177 mic: IconMicrophone,
165178 paintbrush: IconPalette,
179 pencil: IconPencil,
166180 "person.crop.circle": IconUserCircle,
167181 photo: IconPhoto,
168182 pin: IconPin,
......@@ -187,6 +201,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
187201 "sun.max": IconSun,
188202 "stop.fill": IconPlayerStopFilled,
189203 terminal: IconTerminal2,
204 "text.alignleft": IconAlignLeft,
190205 "text.bubble": IconMessage,
191206 "text.word.spacing": IconLetterSpacing,
192207 "textformat.size": IconTypography,
......@@ -197,13 +212,14 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
197212 "wifi.slash": IconWifiOff,
198213 xmark: IconX,
199214 "xmark.circle.fill": IconCircleXFilled,
200};
215} satisfies Partial<Record<SFSymbol, Icon>>;
216const SF_ICON_LOOKUP: Partial<Record<SFSymbol, Icon>> = ANDROID_ICON_BY_SF_SYMBOL;
201217
202218// Callers can pass `{ ios, android }` names where `android` is a Material
203219// icon name (the raw expo-symbols contract). Resolve those here too so the
204220// android key keeps working through this wrapper — it wins over the SF map
205221// when both match (e.g. folder vs folder_open for expanded project groups).
206const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {
222const ANDROID_ICON_BY_MATERIAL_NAME = {
207223 auto_awesome: IconSparkles,
208224 bolt: IconBolt,
209225 build: IconTool,
......@@ -222,21 +238,34 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {
222238 keyboard_arrow_up: IconChevronUp,
223239 keyboard_hide: IconKeyboardHide,
224240 more_vert: IconDotsVertical,
241 merge: IconGitMerge,
225242 public: IconWorld,
226243 remove: IconMinus,
244 smartphone: IconDeviceMobile,
227245 terminal: IconTerminal2,
228246 visibility: IconEye,
229};
247} satisfies Partial<Record<AndroidSymbol, Icon>>;
230248
231249export type { SFSymbol } from "expo-symbols";
232export type AppSymbolName = SymbolViewProps["name"];
250export type AppSymbolName =
251 | keyof typeof ANDROID_ICON_BY_SF_SYMBOL
252 | {
253 ios: SFSymbol;
254 android: keyof typeof ANDROID_ICON_BY_MATERIAL_NAME;
255 };
256
257export type AppSymbolViewProps = Omit<SymbolViewProps, "name"> & { name: AppSymbolName };
258
259export function isAppSymbolName(name: string): name is Extract<AppSymbolName, string> {
260 return Object.prototype.hasOwnProperty.call(ANDROID_ICON_BY_SF_SYMBOL, name);
261}
233262
234function AppSymbolView(props: SymbolViewProps) {
263function AppSymbolView(props: AppSymbolViewProps) {
235264 const materialName = typeof props.name === "string" ? undefined : props.name.android;
236265 const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios;
237266 const AndroidIcon =
238267 (materialName ? ANDROID_ICON_BY_MATERIAL_NAME[materialName] : undefined) ??
239 (sfSymbol ? ANDROID_ICON_BY_SF_SYMBOL[sfSymbol] : undefined);
268 (sfSymbol ? SF_ICON_LOOKUP[sfSymbol] : undefined);
240269
241270 if (!AndroidIcon) {
242271 return props.fallback ?? null;
apps/mobile/src/components/EnvironmentMachineSymbol.tsx+2-4
......@@ -1,9 +1,7 @@
11import type { EnvironmentMachineKind } from "@t3tools/contracts";
2import type { SFSymbol } from "expo-symbols";
2import { SymbolView, type AppSymbolName } from "./AppSymbol";
33
4import { SymbolView } from "./AppSymbol";
5
6const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, SFSymbol> = {
4const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, AppSymbolName> = {
75 server: "server.rack",
86 cloud: "cloud",
97 linux: "terminal",
apps/mobile/src/components/MaterialMenuPopup.android.tsx+3-3
......@@ -12,7 +12,7 @@ import { View } from "react-native";
1212
1313import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
1414import type { MaterialMenuPopupProps } from "./MaterialMenuPopup";
15import { SymbolView, type AppSymbolName } from "./AppSymbol";
15import { isAppSymbolName, SymbolView, type AppSymbolName } from "./AppSymbol";
1616
1717function MenuIcon(props: {
1818 readonly name: AppSymbolName;
......@@ -96,10 +96,10 @@ export function MaterialMenuPopup(props: MaterialMenuPopupProps) {
9696 ) : null}
9797 </Column>
9898 </DropdownMenuItem.Text>
99 {action.image ? (
99 {action.image && isAppSymbolName(action.image) ? (
100100 <DropdownMenuItem.LeadingIcon>
101101 <MenuIcon
102 name={action.image as AppSymbolName}
102 name={action.image}
103103 destructive={action.attributes?.destructive}
104104 disabled={action.attributes?.disabled}
105105 />