authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 00:30:55-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 00:30:55-07:00
logc3b8825bf476cbce5e061c0f99570cf1f6723b89
tree697e8b810c9a9824720ae12982ba314f164beb2e
parent61a91b6ef1bd45424169c6650362b358d49bbe34
signature Signed by PGP key B5690EEEBB952194

fix: preserve tool icons on failed calls (#9606)


5 files changed, 72 insertions(+), 50 deletions(-)

apps/mobile/src/features/threads/thread-work-log.tsx+20-20
...@@ -76,14 +76,13 @@ type WorkContentIcon = AppSymbolName | "browser" | "t3-code";...@@ -76,14 +76,13 @@ type WorkContentIcon = AppSymbolName | "browser" | "t3-code";
76function WorkLogIcon(props: {76function WorkLogIcon(props: {
77 readonly icon: WorkContentIcon;77 readonly icon: WorkContentIcon;
78 readonly color: ColorValue;78 readonly color: ColorValue;
79 readonly colorClassName?: string;
79 readonly highlighted?: boolean;80 readonly highlighted?: boolean;
80}) {81}) {
82 const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName;
81 if (props.icon === "t3-code") {83 if (props.icon === "t3-code") {
82 return (84 return (
83 <T3Wordmark85 <T3Wordmark height={10} {...(colorClassName ? { colorClassName } : { color: props.color })} />
84 height={10}
85 {...(props.highlighted ? { colorClassName: "accent-foreground" } : { color: props.color })}
86 />
87 );86 );
88 }87 }
89 return (88 return (
...@@ -91,9 +90,7 @@ function WorkLogIcon(props: {...@@ -91,9 +90,7 @@ function WorkLogIcon(props: {
91 name={props.icon === "browser" ? { ios: "globe", android: "public" } : props.icon}90 name={props.icon === "browser" ? { ios: "globe", android: "public" } : props.icon}
92 size={14}91 size={14}
93 weight="medium"92 weight="medium"
94 {...(props.highlighted93 {...(colorClassName ? { tintColorClassName: colorClassName } : { tintColor: props.color })}
95 ? { tintColorClassName: "accent-foreground" }
96 : { tintColor: props.color })}
97 type="monochrome"94 type="monochrome"
98 />95 />
99 );96 );
...@@ -707,11 +704,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -707,11 +704,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
707 const iconIsDestructive = row.icon === "alert" || row.icon === "warning";704 const iconIsDestructive = row.icon === "alert" || row.icon === "warning";
708 const failed = row.status === "failure";705 const failed = row.status === "failure";
709 const toolIcon = row.workEntry.toolIcon ?? row.workEntry.toolSource?.icon;706 const toolIcon = row.workEntry.toolIcon ?? row.workEntry.toolSource?.icon;
710 const hasSpecialToolIcon =707 const icon = toolPresentation?.icon ?? workRowSymbolName(row.icon);
711 toolPresentation !== null || row.workEntry.toolSurface !== undefined || toolIcon !== undefined;
712 const icon =
713 toolPresentation?.icon ??
714 (failed && !hasSpecialToolIcon ? "xmark" : workRowSymbolName(row.icon));
715708
716 return (709 return (
717 <Animated.View710 <Animated.View
...@@ -750,12 +743,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -750,12 +743,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
750 ) : (743 ) : (
751 <>744 <>
752 <View className="h-6 w-6 shrink-0 items-center justify-center">745 <View className="h-6 w-6 shrink-0 items-center justify-center">
753 {failed && !hasSpecialToolIcon ? (746 {toolIcon ? (
754 <WorkLogIcon
755 icon={icon}
756 color={iconIsDestructive ? "#e11d48" : props.iconSubtleColor}
757 />
758 ) : (
759 <ToolActivityIconView747 <ToolActivityIconView
760 environmentId={props.environmentId}748 environmentId={props.environmentId}
761 icon={toolIcon}749 icon={toolIcon}
...@@ -763,6 +751,18 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -763,6 +751,18 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
763 fallbackColor={props.iconSubtleColor}751 fallbackColor={props.iconSubtleColor}
764 themeAppearance={props.themeAppearance}752 themeAppearance={props.themeAppearance}
765 />753 />
754 ) : (
755 <WorkLogIcon
756 icon={icon}
757 color={props.iconSubtleColor}
758 colorClassName={
759 iconIsDestructive
760 ? "accent-adaptive-rose-600-400"
761 : failed
762 ? "accent-danger-foreground/40"
763 : undefined
764 }
765 />
766 )}766 )}
767 </View>767 </View>
768 <Text768 <Text
...@@ -783,7 +783,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -783,7 +783,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
783 Copied783 Copied
784 </Text>784 </Text>
785 ) : null}785 ) : null}
786 {failed && hasSpecialToolIcon ? (786 {failed && toolIcon !== undefined ? (
787 <View787 <View
788 className="h-4 w-4 items-center justify-center"788 className="h-4 w-4 items-center justify-center"
789 accessibilityElementsHidden789 accessibilityElementsHidden
...@@ -792,7 +792,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -792,7 +792,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
792 <SymbolView792 <SymbolView
793 name="xmark"793 name="xmark"
794 size={11}794 size={11}
795 tintColorClassName="accent-adaptive-rose-600-400"795 tintColorClassName="accent-danger-foreground/40"
796 type="monochrome"796 type="monochrome"
797 />797 />
798 </View>798 </View>
apps/web/src/components/chat/MessagesTimeline.test.tsx+1-1
...@@ -1471,7 +1471,7 @@ describe("MessagesTimeline", () => {...@@ -1471,7 +1471,7 @@ describe("MessagesTimeline", () => {
1471 );1471 );
14721472
1473 expect(markup).toContain('aria-label="Received 1 update and used 1 tool, tool call failed"');1473 expect(markup).toContain('aria-label="Received 1 update and used 1 tool, tool call failed"');
1474 // Ordinary tool failures render muted, not red.1474 // Ordinary tool failures do not use destructive row styling.
1475 expect(markup).not.toContain("text-destructive");1475 expect(markup).not.toContain("text-destructive");
1476 });1476 });
14771477
apps/web/src/components/chat/MessagesTimeline.tsx+43-29
...@@ -1893,6 +1893,17 @@ function ActivityShimmerOverlay({ children }: { children: ReactNode }) {...@@ -1893,6 +1893,17 @@ function ActivityShimmerOverlay({ children }: { children: ReactNode }) {
1893 );1893 );
1894}1894}
18951895
1896const failedToolIconClassName = "text-tool-error-icon/40";
1897
1898/** Image icons and the gradient computer-use mark cannot take a currentColor
1899 * tint, so failed rows using them get a trailing x instead. */
1900function toolIconAcceptsTint(
1901 iconName: WorkEntryIconName,
1902 toolIcon: ToolActivityIcon | undefined,
1903): boolean {
1904 return toolIcon === undefined && iconName !== "computer";
1905}
1906
1896function LiveActivityRow({1907function LiveActivityRow({
1897 label,1908 label,
1898 iconName,1909 iconName,
...@@ -1941,37 +1952,41 @@ function LiveActivityContent({...@@ -1941,37 +1952,41 @@ function LiveActivityContent({
1941 announceFailure?: boolean;1952 announceFailure?: boolean;
1942 highlighted?: boolean;1953 highlighted?: boolean;
1943}) {1954}) {
1944 const isSpecialToolIcon =1955 const showTrailingFailureMark =
1945 iconName === "browser" || iconName === "computer" || iconName === "t3-code";1956 failed && iconName !== undefined && !toolIconAcceptsTint(iconName, toolIcon);
1946 const resolvedIconName = failed && !isSpecialToolIcon ? "circle-alert" : iconName;
19471957
1948 return (1958 return (
1949 <span1959 <span
1950 className={cn(1960 className={cn(
1951 "flex min-h-6 min-w-0 items-center gap-1.5 py-0.5",1961 "flex min-h-6 min-w-0 items-center gap-1.5 py-0.5",
1952 resolvedIconName ? "px-0.5" : "px-1",1962 iconName ? "px-0.5" : "px-1",
1953 highlighted ? "text-foreground" : "text-secondary-label",1963 highlighted ? "text-foreground" : "text-secondary-label",
1954 )}1964 )}
1955 >1965 >
1956 {resolvedIconName ? (1966 {iconName ? (
1957 <span1967 <span
1958 className={cn(1968 className={cn(
1959 "flex size-6 shrink-0 items-center justify-center",1969 "flex size-6 shrink-0 items-center justify-center",
1960 highlighted ? "text-foreground" : "text-icon-muted",1970 highlighted ? "text-foreground" : failed ? failedToolIconClassName : "text-icon-muted",
1961 )}1971 )}
1962 role={announceFailure ? "img" : undefined}1972 role={announceFailure ? "img" : undefined}
1963 aria-label={announceFailure ? "Tool call failed" : undefined}1973 aria-label={announceFailure ? "Tool call failed" : undefined}
1964 >1974 >
1965 <ToolActivityIconView1975 <ToolActivityIconView
1966 icon={failed && !isSpecialToolIcon ? undefined : toolIcon}1976 icon={toolIcon}
1967 fallbackName={resolvedIconName}1977 fallbackName={iconName}
1968 className="block size-4 shrink-0 stroke-[1.8]"1978 className="block size-4 shrink-0 stroke-[1.8]"
1969 muted={!highlighted}1979 muted={!highlighted}
1970 />1980 />
1971 </span>1981 </span>
1972 ) : null}1982 ) : null}
1973 <span className="min-w-0 flex-1 truncate">{label}</span>1983 <span className="min-w-0 flex-1 truncate">{label}</span>
1974 {failed && isSpecialToolIcon ? <XIcon aria-hidden className="size-3 shrink-0" /> : null}1984 {showTrailingFailureMark ? (
1985 <XIcon
1986 aria-hidden
1987 className={cn("size-3 shrink-0", !highlighted && failedToolIconClassName)}
1988 />
1989 ) : null}
1975 </span>1990 </span>
1976 );1991 );
1977}1992}
...@@ -3088,12 +3103,15 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3088,12 +3103,15 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3088 const iconConfig = workToneIcon(workEntry.tone);3103 const iconConfig = workToneIcon(workEntry.tone);
3089 const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";3104 const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
3090 const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);3105 const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
3091 const toolPresentation = resolveWorkEntryToolPresentation(workEntry);3106 const showDestructiveRowStyle =
3092 const hasSpecialToolIcon = toolPresentation !== null || workEntry.toolSurface !== undefined;3107 showFailedIndicator &&
3108 (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry));
3093 const entryIconName =3109 const entryIconName =
3094 showWarningIndicator || (showFailedIndicator && !hasSpecialToolIcon)3110 showWarningIndicator || showDestructiveRowStyle ? "circle-alert" : workEntryIconName(workEntry);
3095 ? "circle-alert"3111 const entryToolIcon =
3096 : workEntryIconName(workEntry);3112 showWarningIndicator || showDestructiveRowStyle
3113 ? undefined
3114 : (workEntry.toolIcon ?? workEntry.toolSource?.icon);
3097 const previewText = displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot);3115 const previewText = displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot);
3098 const viewedImagePath = workEntryViewedImagePath(workEntry);3116 const viewedImagePath = workEntryViewedImagePath(workEntry);
3099 const viewedImage =3117 const viewedImage =
...@@ -3121,20 +3139,18 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3121,20 +3139,18 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3121 viewedImage ? viewedImagePath : null,3139 viewedImage ? viewedImagePath : null,
3122 )3140 )
3123 : null;3141 : null;
3124 const showDestructiveRowStyle =3142 // Reserve destructive row styling for severe failures, not routine tool errors.
3125 showFailedIndicator &&
3126 (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry));
3127 // Ordinary tool failures stay muted; only runtime errors and warnings get
3128 // color. The red treatment is reserved for severe failures.
3129 const iconWrapperClass = cn(3143 const iconWrapperClass = cn(
3130 "flex size-6 shrink-0 items-center justify-center",3144 "flex size-6 shrink-0 items-center justify-center",
3131 showWarningIndicator3145 showWarningIndicator
3132 ? "text-warning"3146 ? "text-warning"
3133 : showDestructiveRowStyle3147 : showDestructiveRowStyle
3134 ? "text-destructive"3148 ? "text-destructive"
3135 : workEntry.tone === "tool" || showFailedIndicator3149 : showFailedIndicator
3136 ? "text-icon-muted"3150 ? failedToolIconClassName
3137 : iconConfig.className,3151 : workEntry.tone === "tool"
3152 ? "text-icon-muted"
3153 : iconConfig.className,
3138 );3154 );
3139 const headingClass = showWarningIndicator3155 const headingClass = showWarningIndicator
3140 ? "font-medium text-warning"3156 ? "font-medium text-warning"
...@@ -3179,11 +3195,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3179,11 +3195,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3179 aria-label={showFailedIndicator ? "Tool call failed" : undefined}3195 aria-label={showFailedIndicator ? "Tool call failed" : undefined}
3180 >3196 >
3181 <ToolActivityIconView3197 <ToolActivityIconView
3182 icon={3198 icon={entryToolIcon}
3183 showWarningIndicator || (showFailedIndicator && !hasSpecialToolIcon)
3184 ? undefined
3185 : (workEntry.toolIcon ?? workEntry.toolSource?.icon)
3186 }
3187 fallbackName={entryIconName}3199 fallbackName={entryIconName}
3188 className="block size-4 shrink-0 stroke-[1.8]"3200 className="block size-4 shrink-0 stroke-[1.8]"
3189 muted3201 muted
...@@ -3207,8 +3219,10 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3207,8 +3219,10 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3207 </span>3219 </span>
3208 </p>3220 </p>
3209 </div>3221 </div>
3210 {showFailedIndicator && hasSpecialToolIcon ? (3222 {showFailedIndicator &&
3211 <XIcon aria-hidden className="size-3 shrink-0 text-icon-muted" />3223 !showDestructiveRowStyle &&
3224 !toolIconAcceptsTint(entryIconName, entryToolIcon) ? (
3225 <XIcon aria-hidden className={cn("size-3 shrink-0", failedToolIconClassName)} />
3212 ) : null}3226 ) : null}
3213 <span3227 <span
3214 className={cn(3228 className={cn(
apps/web/src/index.css+4
...@@ -154,6 +154,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -154,6 +154,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
154 --color-warning: var(--warning);154 --color-warning: var(--warning);
155 --color-warning-surface: var(--warning-surface);155 --color-warning-surface: var(--warning-surface);
156 --color-error-foreground: var(--error-foreground);156 --color-error-foreground: var(--error-foreground);
157 --color-tool-error-icon: var(--tool-error-icon);
157 --color-error: var(--error);158 --color-error: var(--error);
158 --color-error-surface: var(--error-surface);159 --color-error-surface: var(--error-surface);
159 --color-update-foreground: var(--update-foreground);160 --color-update-foreground: var(--update-foreground);
...@@ -948,6 +949,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -948,6 +949,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
948 --accent-foreground: var(--color-zinc-900);949 --accent-foreground: var(--color-zinc-900);
949 --error: var(--color-red-500);950 --error: var(--color-red-500);
950 --error-foreground: var(--color-red-700);951 --error-foreground: var(--color-red-700);
952 --tool-error-icon: var(--error);
951 --error-surface: color-mix(in srgb, var(--error) 8%, transparent);953 --error-surface: color-mix(in srgb, var(--error) 8%, transparent);
952 --destructive: var(--error);954 --destructive: var(--error);
953 --border: var(--color-zinc-200);955 --border: var(--color-zinc-200);
...@@ -1002,6 +1004,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -1002,6 +1004,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
1002 --accent-foreground: var(--color-neutral-100);1004 --accent-foreground: var(--color-neutral-100);
1003 --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));1005 --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
1004 --error-foreground: var(--color-red-400);1006 --error-foreground: var(--color-red-400);
1007 --tool-error-icon: #fca5a5;
1005 --error-surface: color-mix(in srgb, var(--error) 16%, transparent);1008 --error-surface: color-mix(in srgb, var(--error) 16%, transparent);
1006 --border: --alpha(var(--color-white) / 6%);1009 --border: --alpha(var(--color-white) / 6%);
1007 --input: --alpha(var(--color-white) / 8%);1010 --input: --alpha(var(--color-white) / 8%);
...@@ -1114,6 +1117,7 @@ html[data-theme-id]:not([data-theme-id=""]) {...@@ -1114,6 +1117,7 @@ html[data-theme-id]:not([data-theme-id=""]) {
1114 --message-action-hover: var(--app-theme-message-action-hover);1117 --message-action-hover: var(--app-theme-message-action-hover);
1115 --error: var(--app-theme-error);1118 --error: var(--app-theme-error);
1116 --error-foreground: var(--app-theme-error-foreground);1119 --error-foreground: var(--app-theme-error-foreground);
1120 --tool-error-icon: var(--error-foreground);
1117 --error-surface: var(--app-theme-error-surface);1121 --error-surface: var(--app-theme-error-surface);
1118 --destructive: var(--error);1122 --destructive: var(--error);
1119 --destructive-foreground: var(--error-foreground);1123 --destructive-foreground: var(--error-foreground);
docs/user/tool-activity.md+4
...@@ -13,6 +13,10 @@ The latest live activity stays in the present tense while the turn continues, su...@@ -13,6 +13,10 @@ The latest live activity stays in the present tense while the turn continues, su
13Expanded rows follow the call's own state, such as "Clicked" after success.13Expanded rows follow the call's own state, such as "Clicked" after success.
14When a call has not reported a state yet, the label stays in the present tense.14When a call has not reported a state yet, the label stays in the present tense.
15Failed, declined, and stopped calls say what happened without implying success.15Failed, declined, and stopped calls say what happened without implying success.
16Failed calls keep their tool icon with a muted red tint, so a failed command still
17looks like a command. App logos and other multicolor icons keep their artwork and
18show a small muted red failure mark beside the row. Expand the call to inspect its
19output. Runtime errors and warnings keep their stronger styling.
16Preview browser actions use a globe icon. Other T3 tools keep the T3 mark.20Preview browser actions use a globe icon. Other T3 tools keep the T3 mark.
17Group summaries count browser actions separately, such as "Used browser 18 times" or21Group summaries count browser actions separately, such as "Used browser 18 times" or
18"Ran 4 commands and used browser 15 times". Browser-only groups also use a globe icon.22"Ran 4 commands and used browser 15 times". Browser-only groups also use a globe icon.