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 @@...@@ -1,14 +1,15 @@
1import { SymbolView as ExpoSymbolView, type SymbolViewProps } from "expo-symbols";1import { SymbolView as ExpoSymbolView } from "expo-symbols";
2import { withUniwind } from "uniwind";2import { withUniwind } from "uniwind";
3import type { AppSymbolViewProps } from "./AppSymbol";
34
4export type { SFSymbol } from "expo-symbols";5export type { SFSymbol } from "expo-symbols";
5export type AppSymbolName = SymbolViewProps["name"];6export type { AppSymbolName } from "./AppSymbol";
67
7/**8/**
8 * Keep the iOS implementation isolated from the Android Tabler fallback so9 * Keep the iOS implementation isolated from the Android Tabler fallback so
9 * Metro does not initialize the icon package when iOS renders SF Symbols.10 * Metro does not initialize the icon package when iOS renders SF Symbols.
10 */11 */
11function AppSymbolView(props: SymbolViewProps) {12function AppSymbolView(props: AppSymbolViewProps) {
12 return <ExpoSymbolView {...props} />;13 return <ExpoSymbolView {...props} />;
13}14}
1415
apps/mobile/src/components/AppSymbol.tsx+39-10
...@@ -4,11 +4,14 @@ import type { Icon } from "@tabler/icons-react-native/types";...@@ -4,11 +4,14 @@ import type { Icon } from "@tabler/icons-react-native/types";
4 * the entire Tabler icon set in Metro.4 * the entire Tabler icon set in Metro.
5 */5 */
6import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal";6import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal";
7import IconAlignLeft from "@tabler/icons-react-native/IconAlignLeft";
7import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle";8import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle";
8import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle";9import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle";
9import IconApps from "@tabler/icons-react-native/IconApps";10import IconApps from "@tabler/icons-react-native/IconApps";
10import IconArchive from "@tabler/icons-react-native/IconArchive";11import IconArchive from "@tabler/icons-react-native/IconArchive";
11import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp";12import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp";
13import IconArrowDown from "@tabler/icons-react-native/IconArrowDown";
14import IconArrowForwardUp from "@tabler/icons-react-native/IconArrowForwardUp";
12import IconArrowLeft from "@tabler/icons-react-native/IconArrowLeft";15import IconArrowLeft from "@tabler/icons-react-native/IconArrowLeft";
13import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle";16import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle";
14import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle";17import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle";
...@@ -30,15 +33,17 @@ import IconChevronDown from "@tabler/icons-react-native/IconChevronDown";...@@ -30,15 +33,17 @@ import IconChevronDown from "@tabler/icons-react-native/IconChevronDown";
30import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft";33import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft";
31import IconChevronRight from "@tabler/icons-react-native/IconChevronRight";34import IconChevronRight from "@tabler/icons-react-native/IconChevronRight";
32import IconChevronUp from "@tabler/icons-react-native/IconChevronUp";35import IconChevronUp from "@tabler/icons-react-native/IconChevronUp";
36import IconCircle from "@tabler/icons-react-native/IconCircle";
33import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck";37import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck";
34import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled";38import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled";
35import IconTicket from "@tabler/icons-react-native/IconTicket";39import IconTicket from "@tabler/icons-react-native/IconTicket";
36import IconClock from "@tabler/icons-react-native/IconClock";40import IconClock from "@tabler/icons-react-native/IconClock";
37import IconCode from "@tabler/icons-react-native/IconCode";41import IconCode from "@tabler/icons-react-native/IconCode";
38import IconCopy from "@tabler/icons-react-native/IconCopy";42import IconCopy from "@tabler/icons-react-native/IconCopy";
39import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop";
40import IconDatabase from "@tabler/icons-react-native/IconDatabase";43import IconDatabase from "@tabler/icons-react-native/IconDatabase";
44import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop";
41import IconDeviceLaptop from "@tabler/icons-react-native/IconDeviceLaptop";45import IconDeviceLaptop from "@tabler/icons-react-native/IconDeviceLaptop";
46import IconDeviceMobile from "@tabler/icons-react-native/IconDeviceMobile";
42import IconDots from "@tabler/icons-react-native/IconDots";47import IconDots from "@tabler/icons-react-native/IconDots";
43import IconDotsVertical from "@tabler/icons-react-native/IconDotsVertical";48import IconDotsVertical from "@tabler/icons-react-native/IconDotsVertical";
44import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal";49import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal";
...@@ -62,13 +67,15 @@ import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns";...@@ -62,13 +67,15 @@ import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns";
62import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar";67import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar";
63import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight";68import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight";
64import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing";69import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing";
65import IconMenu2 from "@tabler/icons-react-native/IconMenu2";70import IconMicrophone from "@tabler/icons-react-native/IconMicrophone";
66import IconLink from "@tabler/icons-react-native/IconLink";71import IconLink from "@tabler/icons-react-native/IconLink";
72import IconMenu2 from "@tabler/icons-react-native/IconMenu2";
67import IconMessage from "@tabler/icons-react-native/IconMessage";73import IconMessage from "@tabler/icons-react-native/IconMessage";
68import IconMinus from "@tabler/icons-react-native/IconMinus";74import IconMinus from "@tabler/icons-react-native/IconMinus";
69import IconMoon from "@tabler/icons-react-native/IconMoon";75import IconMoon from "@tabler/icons-react-native/IconMoon";
70import IconNetwork from "@tabler/icons-react-native/IconNetwork";76import IconNetwork from "@tabler/icons-react-native/IconNetwork";
71import IconPalette from "@tabler/icons-react-native/IconPalette";77import IconPalette from "@tabler/icons-react-native/IconPalette";
78import IconPencil from "@tabler/icons-react-native/IconPencil";
72import IconPhoto from "@tabler/icons-react-native/IconPhoto";79import IconPhoto from "@tabler/icons-react-native/IconPhoto";
73import IconPin from "@tabler/icons-react-native/IconPin";80import IconPin from "@tabler/icons-react-native/IconPin";
74import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff";81import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff";
...@@ -97,13 +104,14 @@ import IconUserCircle from "@tabler/icons-react-native/IconUserCircle";...@@ -97,13 +104,14 @@ import IconUserCircle from "@tabler/icons-react-native/IconUserCircle";
97import IconWifiOff from "@tabler/icons-react-native/IconWifiOff";104import IconWifiOff from "@tabler/icons-react-native/IconWifiOff";
98import IconWorld from "@tabler/icons-react-native/IconWorld";105import IconWorld from "@tabler/icons-react-native/IconWorld";
99import IconX from "@tabler/icons-react-native/IconX";106import IconX from "@tabler/icons-react-native/IconX";
100import type { SFSymbol, SymbolViewProps } from "expo-symbols";107import type { AndroidSymbol, SFSymbol, SymbolViewProps } from "expo-symbols";
101import { withUniwind } from "uniwind";108import { withUniwind } from "uniwind";
102109
103const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {110const ANDROID_ICON_BY_SF_SYMBOL = {
104 "arrow.branch": IconGitBranch,111 "arrow.branch": IconGitBranch,
105 "arrow.left": IconArrowLeft,112 "arrow.left": IconArrowLeft,
106 "arrow.clockwise": IconRefresh,113 "arrow.clockwise": IconRefresh,
114 "arrow.down": IconArrowDown,
107 "arrow.down.circle": IconArrowDownCircle,115 "arrow.down.circle": IconArrowDownCircle,
108 "arrow.right.circle": IconArrowRightCircle,116 "arrow.right.circle": IconArrowRightCircle,
109 "arrow.triangle.branch": IconGitBranch,117 "arrow.triangle.branch": IconGitBranch,
...@@ -117,6 +125,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {...@@ -117,6 +125,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
117 "arrow.up.right": IconArrowUpRight,125 "arrow.up.right": IconArrowUpRight,
118 "arrow.up.right.circle": IconArrowUpRightCircle,126 "arrow.up.right.circle": IconArrowUpRightCircle,
119 "arrow.uturn.backward": IconArrowBackUp,127 "arrow.uturn.backward": IconArrowBackUp,
128 "arrow.uturn.forward": IconArrowForwardUp,
120 archivebox: IconArchive,129 archivebox: IconArchive,
121 "archivebox.fill": IconArchive,130 "archivebox.fill": IconArchive,
122 "bell.badge": IconBellRinging,131 "bell.badge": IconBellRinging,
...@@ -127,6 +136,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {...@@ -127,6 +136,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
127 "chart.bar.xaxis": IconChartBar,136 "chart.bar.xaxis": IconChartBar,
128 checkmark: IconCheck,137 checkmark: IconCheck,
129 "checkmark.circle": IconCircleCheck,138 "checkmark.circle": IconCircleCheck,
139 circle: IconCircle,
130 clock: IconClock,140 clock: IconClock,
131 ticket: IconTicket,141 ticket: IconTicket,
132 cloud: IconCloud,142 cloud: IconCloud,
...@@ -143,11 +153,13 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {...@@ -143,11 +153,13 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
143 moon: IconMoon,153 moon: IconMoon,
144 "ellipsis.circle": IconDotsCircleHorizontal,154 "ellipsis.circle": IconDotsCircleHorizontal,
145 "exclamationmark.triangle": IconAlertTriangle,155 "exclamationmark.triangle": IconAlertTriangle,
156 "exclamationmark.circle": IconAlertCircle,
146 eye: IconEye,157 eye: IconEye,
147 folder: IconFolder,158 folder: IconFolder,
148 "folder.badge.plus": IconFolderPlus,159 "folder.badge.plus": IconFolderPlus,
149 "folder.fill": IconFolder,160 "folder.fill": IconFolder,
150 gearshape: IconSettings,161 gearshape: IconSettings,
162 globe: IconWorld,
151 hammer: IconHammer,163 hammer: IconHammer,
152 "info.circle": IconInfoCircle,164 "info.circle": IconInfoCircle,
153 internaldrive: IconDatabase,165 internaldrive: IconDatabase,
...@@ -162,7 +174,9 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {...@@ -162,7 +174,9 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
162 macmini: IconServer,174 macmini: IconServer,
163 macstudio: IconDeviceDesktop,175 macstudio: IconDeviceDesktop,
164 magnifyingglass: IconSearch,176 magnifyingglass: IconSearch,
177 mic: IconMicrophone,
165 paintbrush: IconPalette,178 paintbrush: IconPalette,
179 pencil: IconPencil,
166 "person.crop.circle": IconUserCircle,180 "person.crop.circle": IconUserCircle,
167 photo: IconPhoto,181 photo: IconPhoto,
168 pin: IconPin,182 pin: IconPin,
...@@ -187,6 +201,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {...@@ -187,6 +201,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
187 "sun.max": IconSun,201 "sun.max": IconSun,
188 "stop.fill": IconPlayerStopFilled,202 "stop.fill": IconPlayerStopFilled,
189 terminal: IconTerminal2,203 terminal: IconTerminal2,
204 "text.alignleft": IconAlignLeft,
190 "text.bubble": IconMessage,205 "text.bubble": IconMessage,
191 "text.word.spacing": IconLetterSpacing,206 "text.word.spacing": IconLetterSpacing,
192 "textformat.size": IconTypography,207 "textformat.size": IconTypography,
...@@ -197,13 +212,14 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {...@@ -197,13 +212,14 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
197 "wifi.slash": IconWifiOff,212 "wifi.slash": IconWifiOff,
198 xmark: IconX,213 xmark: IconX,
199 "xmark.circle.fill": IconCircleXFilled,214 "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
202// Callers can pass `{ ios, android }` names where `android` is a Material218// Callers can pass `{ ios, android }` names where `android` is a Material
203// icon name (the raw expo-symbols contract). Resolve those here too so the219// icon name (the raw expo-symbols contract). Resolve those here too so the
204// android key keeps working through this wrapper — it wins over the SF map220// android key keeps working through this wrapper — it wins over the SF map
205// when both match (e.g. folder vs folder_open for expanded project groups).221// 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 = {
207 auto_awesome: IconSparkles,223 auto_awesome: IconSparkles,
208 bolt: IconBolt,224 bolt: IconBolt,
209 build: IconTool,225 build: IconTool,
...@@ -222,21 +238,34 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {...@@ -222,21 +238,34 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {
222 keyboard_arrow_up: IconChevronUp,238 keyboard_arrow_up: IconChevronUp,
223 keyboard_hide: IconKeyboardHide,239 keyboard_hide: IconKeyboardHide,
224 more_vert: IconDotsVertical,240 more_vert: IconDotsVertical,
241 merge: IconGitMerge,
225 public: IconWorld,242 public: IconWorld,
226 remove: IconMinus,243 remove: IconMinus,
244 smartphone: IconDeviceMobile,
227 terminal: IconTerminal2,245 terminal: IconTerminal2,
228 visibility: IconEye,246 visibility: IconEye,
229};247} satisfies Partial<Record<AndroidSymbol, Icon>>;
230248
231export type { SFSymbol } from "expo-symbols";249export 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) {
235 const materialName = typeof props.name === "string" ? undefined : props.name.android;264 const materialName = typeof props.name === "string" ? undefined : props.name.android;
236 const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios;265 const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios;
237 const AndroidIcon =266 const AndroidIcon =
238 (materialName ? ANDROID_ICON_BY_MATERIAL_NAME[materialName] : undefined) ??267 (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
241 if (!AndroidIcon) {270 if (!AndroidIcon) {
242 return props.fallback ?? null;271 return props.fallback ?? null;
apps/mobile/src/components/EnvironmentMachineSymbol.tsx+2-4
...@@ -1,9 +1,7 @@...@@ -1,9 +1,7 @@
1import type { EnvironmentMachineKind } from "@t3tools/contracts";1import type { EnvironmentMachineKind } from "@t3tools/contracts";
2import type { SFSymbol } from "expo-symbols";2import { SymbolView, type AppSymbolName } from "./AppSymbol";
33
4import { SymbolView } from "./AppSymbol";4const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, AppSymbolName> = {
5
6const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, SFSymbol> = {
7 server: "server.rack",5 server: "server.rack",
8 cloud: "cloud",6 cloud: "cloud",
9 linux: "terminal",7 linux: "terminal",
apps/mobile/src/components/MaterialMenuPopup.android.tsx+3-3
...@@ -12,7 +12,7 @@ import { View } from "react-native";...@@ -12,7 +12,7 @@ import { View } from "react-native";
1212
13import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";13import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
14import type { MaterialMenuPopupProps } from "./MaterialMenuPopup";14import type { MaterialMenuPopupProps } from "./MaterialMenuPopup";
15import { SymbolView, type AppSymbolName } from "./AppSymbol";15import { isAppSymbolName, SymbolView, type AppSymbolName } from "./AppSymbol";
1616
17function MenuIcon(props: {17function MenuIcon(props: {
18 readonly name: AppSymbolName;18 readonly name: AppSymbolName;
...@@ -96,10 +96,10 @@ export function MaterialMenuPopup(props: MaterialMenuPopupProps) {...@@ -96,10 +96,10 @@ export function MaterialMenuPopup(props: MaterialMenuPopupProps) {
96 ) : null}96 ) : null}
97 </Column>97 </Column>
98 </DropdownMenuItem.Text>98 </DropdownMenuItem.Text>
99 {action.image ? (99 {action.image && isAppSymbolName(action.image) ? (
100 <DropdownMenuItem.LeadingIcon>100 <DropdownMenuItem.LeadingIcon>
101 <MenuIcon101 <MenuIcon
102 name={action.image as AppSymbolName}102 name={action.image}
103 destructive={action.attributes?.destructive}103 destructive={action.attributes?.destructive}
104 disabled={action.attributes?.disabled}104 disabled={action.attributes?.disabled}
105 />105 />