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";
7676function WorkLogIcon(props: {
7777 readonly icon: WorkContentIcon;
7878 readonly color: ColorValue;
79 readonly colorClassName?: string;
7980 readonly highlighted?: boolean;
8081}) {
82 const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName;
8183 if (props.icon === "t3-code") {
8284 return (
83 <T3Wordmark
84 height={10}
85 {...(props.highlighted ? { colorClassName: "accent-foreground" } : { color: props.color })}
86 />
85 <T3Wordmark height={10} {...(colorClassName ? { colorClassName } : { color: props.color })} />
8786 );
8887 }
8988 return (
......@@ -91,9 +90,7 @@ function WorkLogIcon(props: {
9190 name={props.icon === "browser" ? { ios: "globe", android: "public" } : props.icon}
9291 size={14}
9392 weight="medium"
94 {...(props.highlighted
95 ? { tintColorClassName: "accent-foreground" }
96 : { tintColor: props.color })}
93 {...(colorClassName ? { tintColorClassName: colorClassName } : { tintColor: props.color })}
9794 type="monochrome"
9895 />
9996 );
......@@ -707,11 +704,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
707704 const iconIsDestructive = row.icon === "alert" || row.icon === "warning";
708705 const failed = row.status === "failure";
709706 const toolIcon = row.workEntry.toolIcon ?? row.workEntry.toolSource?.icon;
710 const hasSpecialToolIcon =
711 toolPresentation !== null || row.workEntry.toolSurface !== undefined || toolIcon !== undefined;
712 const icon =
713 toolPresentation?.icon ??
714 (failed && !hasSpecialToolIcon ? "xmark" : workRowSymbolName(row.icon));
707 const icon = toolPresentation?.icon ?? workRowSymbolName(row.icon);
715708
716709 return (
717710 <Animated.View
......@@ -750,12 +743,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
750743 ) : (
751744 <>
752745 <View className="h-6 w-6 shrink-0 items-center justify-center">
753 {failed && !hasSpecialToolIcon ? (
754 <WorkLogIcon
755 icon={icon}
756 color={iconIsDestructive ? "#e11d48" : props.iconSubtleColor}
757 />
758 ) : (
746 {toolIcon ? (
759747 <ToolActivityIconView
760748 environmentId={props.environmentId}
761749 icon={toolIcon}
......@@ -763,6 +751,18 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
763751 fallbackColor={props.iconSubtleColor}
764752 themeAppearance={props.themeAppearance}
765753 />
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 />
766766 )}
767767 </View>
768768 <Text
......@@ -783,7 +783,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
783783 Copied
784784 </Text>
785785 ) : null}
786 {failed && hasSpecialToolIcon ? (
786 {failed && toolIcon !== undefined ? (
787787 <View
788788 className="h-4 w-4 items-center justify-center"
789789 accessibilityElementsHidden
......@@ -792,7 +792,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
792792 <SymbolView
793793 name="xmark"
794794 size={11}
795 tintColorClassName="accent-adaptive-rose-600-400"
795 tintColorClassName="accent-danger-foreground/40"
796796 type="monochrome"
797797 />
798798 </View>
apps/web/src/components/chat/MessagesTimeline.test.tsx+1-1
......@@ -1471,7 +1471,7 @@ describe("MessagesTimeline", () => {
14711471 );
14721472
14731473 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.
14751475 expect(markup).not.toContain("text-destructive");
14761476 });
14771477
apps/web/src/components/chat/MessagesTimeline.tsx+43-29
......@@ -1893,6 +1893,17 @@ function ActivityShimmerOverlay({ children }: { children: ReactNode }) {
18931893 );
18941894}
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
18961907function LiveActivityRow({
18971908 label,
18981909 iconName,
......@@ -1941,37 +1952,41 @@ function LiveActivityContent({
19411952 announceFailure?: boolean;
19421953 highlighted?: boolean;
19431954}) {
1944 const isSpecialToolIcon =
1945 iconName === "browser" || iconName === "computer" || iconName === "t3-code";
1946 const resolvedIconName = failed && !isSpecialToolIcon ? "circle-alert" : iconName;
1955 const showTrailingFailureMark =
1956 failed && iconName !== undefined && !toolIconAcceptsTint(iconName, toolIcon);
19471957
19481958 return (
19491959 <span
19501960 className={cn(
19511961 "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",
19531963 highlighted ? "text-foreground" : "text-secondary-label",
19541964 )}
19551965 >
1956 {resolvedIconName ? (
1966 {iconName ? (
19571967 <span
19581968 className={cn(
19591969 "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",
19611971 )}
19621972 role={announceFailure ? "img" : undefined}
19631973 aria-label={announceFailure ? "Tool call failed" : undefined}
19641974 >
19651975 <ToolActivityIconView
1966 icon={failed && !isSpecialToolIcon ? undefined : toolIcon}
1967 fallbackName={resolvedIconName}
1976 icon={toolIcon}
1977 fallbackName={iconName}
19681978 className="block size-4 shrink-0 stroke-[1.8]"
19691979 muted={!highlighted}
19701980 />
19711981 </span>
19721982 ) : null}
19731983 <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}
19751990 </span>
19761991 );
19771992}
......@@ -3088,12 +3103,15 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
30883103 const iconConfig = workToneIcon(workEntry.tone);
30893104 const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
30903105 const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
3091 const toolPresentation = resolveWorkEntryToolPresentation(workEntry);
3092 const hasSpecialToolIcon = toolPresentation !== null || workEntry.toolSurface !== undefined;
3106 const showDestructiveRowStyle =
3107 showFailedIndicator &&
3108 (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry));
30933109 const entryIconName =
3094 showWarningIndicator || (showFailedIndicator && !hasSpecialToolIcon)
3095 ? "circle-alert"
3096 : workEntryIconName(workEntry);
3110 showWarningIndicator || showDestructiveRowStyle ? "circle-alert" : workEntryIconName(workEntry);
3111 const entryToolIcon =
3112 showWarningIndicator || showDestructiveRowStyle
3113 ? undefined
3114 : (workEntry.toolIcon ?? workEntry.toolSource?.icon);
30973115 const previewText = displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot);
30983116 const viewedImagePath = workEntryViewedImagePath(workEntry);
30993117 const viewedImage =
......@@ -3121,20 +3139,18 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
31213139 viewedImage ? viewedImagePath : null,
31223140 )
31233141 : null;
3124 const showDestructiveRowStyle =
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.
3142 // Reserve destructive row styling for severe failures, not routine tool errors.
31293143 const iconWrapperClass = cn(
31303144 "flex size-6 shrink-0 items-center justify-center",
31313145 showWarningIndicator
31323146 ? "text-warning"
31333147 : showDestructiveRowStyle
31343148 ? "text-destructive"
3135 : workEntry.tone === "tool" || showFailedIndicator
3136 ? "text-icon-muted"
3137 : iconConfig.className,
3149 : showFailedIndicator
3150 ? failedToolIconClassName
3151 : workEntry.tone === "tool"
3152 ? "text-icon-muted"
3153 : iconConfig.className,
31383154 );
31393155 const headingClass = showWarningIndicator
31403156 ? "font-medium text-warning"
......@@ -3179,11 +3195,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
31793195 aria-label={showFailedIndicator ? "Tool call failed" : undefined}
31803196 >
31813197 <ToolActivityIconView
3182 icon={
3183 showWarningIndicator || (showFailedIndicator && !hasSpecialToolIcon)
3184 ? undefined
3185 : (workEntry.toolIcon ?? workEntry.toolSource?.icon)
3186 }
3198 icon={entryToolIcon}
31873199 fallbackName={entryIconName}
31883200 className="block size-4 shrink-0 stroke-[1.8]"
31893201 muted
......@@ -3207,8 +3219,10 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
32073219 </span>
32083220 </p>
32093221 </div>
3210 {showFailedIndicator && hasSpecialToolIcon ? (
3211 <XIcon aria-hidden className="size-3 shrink-0 text-icon-muted" />
3222 {showFailedIndicator &&
3223 !showDestructiveRowStyle &&
3224 !toolIconAcceptsTint(entryIconName, entryToolIcon) ? (
3225 <XIcon aria-hidden className={cn("size-3 shrink-0", failedToolIconClassName)} />
32123226 ) : null}
32133227 <span
32143228 className={cn(
apps/web/src/index.css+4
......@@ -154,6 +154,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
154154 --color-warning: var(--warning);
155155 --color-warning-surface: var(--warning-surface);
156156 --color-error-foreground: var(--error-foreground);
157 --color-tool-error-icon: var(--tool-error-icon);
157158 --color-error: var(--error);
158159 --color-error-surface: var(--error-surface);
159160 --color-update-foreground: var(--update-foreground);
......@@ -948,6 +949,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
948949 --accent-foreground: var(--color-zinc-900);
949950 --error: var(--color-red-500);
950951 --error-foreground: var(--color-red-700);
952 --tool-error-icon: var(--error);
951953 --error-surface: color-mix(in srgb, var(--error) 8%, transparent);
952954 --destructive: var(--error);
953955 --border: var(--color-zinc-200);
......@@ -1002,6 +1004,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
10021004 --accent-foreground: var(--color-neutral-100);
10031005 --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
10041006 --error-foreground: var(--color-red-400);
1007 --tool-error-icon: #fca5a5;
10051008 --error-surface: color-mix(in srgb, var(--error) 16%, transparent);
10061009 --border: --alpha(var(--color-white) / 6%);
10071010 --input: --alpha(var(--color-white) / 8%);
......@@ -1114,6 +1117,7 @@ html[data-theme-id]:not([data-theme-id=""]) {
11141117 --message-action-hover: var(--app-theme-message-action-hover);
11151118 --error: var(--app-theme-error);
11161119 --error-foreground: var(--app-theme-error-foreground);
1120 --tool-error-icon: var(--error-foreground);
11171121 --error-surface: var(--app-theme-error-surface);
11181122 --destructive: var(--error);
11191123 --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
1313Expanded rows follow the call's own state, such as "Clicked" after success.
1414When a call has not reported a state yet, the label stays in the present tense.
1515Failed, 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.
1620Preview browser actions use a globe icon. Other T3 tools keep the T3 mark.
1721Group summaries count browser actions separately, such as "Used browser 18 times" or
1822"Ran 4 commands and used browser 15 times". Browser-only groups also use a globe icon.