| author | |
| committer | |
| log | c3b8825bf476cbce5e061c0f99570cf1f6723b89 |
| tree | 697e8b810c9a9824720ae12982ba314f164beb2e |
| parent | 61a91b6ef1bd45424169c6650362b358d49bbe34 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 76 | function WorkLogIcon(props: { |
| 77 | 77 | readonly icon: WorkContentIcon; |
| 78 | 78 | readonly color: ColorValue; |
| 79 | readonly colorClassName?: string; | |
| 79 | 80 | readonly highlighted?: boolean; |
| 80 | 81 | }) { |
| 82 | const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName; | |
| 81 | 83 | if (props.icon === "t3-code") { |
| 82 | 84 | 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 })} /> | |
| 87 | 86 | ); |
| 88 | 87 | } |
| 89 | 88 | return ( |
| ... | ... | @@ -91,9 +90,7 @@ function WorkLogIcon(props: { |
| 91 | 90 | name={props.icon === "browser" ? { ios: "globe", android: "public" } : props.icon} |
| 92 | 91 | size={14} |
| 93 | 92 | weight="medium" |
| 94 | {...(props.highlighted | |
| 95 | ? { tintColorClassName: "accent-foreground" } | |
| 96 | : { tintColor: props.color })} | |
| 93 | {...(colorClassName ? { tintColorClassName: colorClassName } : { tintColor: props.color })} | |
| 97 | 94 | type="monochrome" |
| 98 | 95 | /> |
| 99 | 96 | ); |
| ... | ... | @@ -707,11 +704,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 707 | 704 | const iconIsDestructive = row.icon === "alert" || row.icon === "warning"; |
| 708 | 705 | const failed = row.status === "failure"; |
| 709 | 706 | 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); | |
| 715 | 708 | |
| 716 | 709 | return ( |
| 717 | 710 | <Animated.View |
| ... | ... | @@ -750,12 +743,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 750 | 743 | ) : ( |
| 751 | 744 | <> |
| 752 | 745 | <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 ? ( | |
| 759 | 747 | <ToolActivityIconView |
| 760 | 748 | environmentId={props.environmentId} |
| 761 | 749 | icon={toolIcon} |
| ... | ... | @@ -763,6 +751,18 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 763 | 751 | fallbackColor={props.iconSubtleColor} |
| 764 | 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 | 767 | </View> |
| 768 | 768 | <Text |
| ... | ... | @@ -783,7 +783,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 783 | 783 | Copied |
| 784 | 784 | </Text> |
| 785 | 785 | ) : null} |
| 786 | {failed && hasSpecialToolIcon ? ( | |
| 786 | {failed && toolIcon !== undefined ? ( | |
| 787 | 787 | <View |
| 788 | 788 | className="h-4 w-4 items-center justify-center" |
| 789 | 789 | accessibilityElementsHidden |
| ... | ... | @@ -792,7 +792,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 792 | 792 | <SymbolView |
| 793 | 793 | name="xmark" |
| 794 | 794 | size={11} |
| 795 | tintColorClassName="accent-adaptive-rose-600-400" | |
| 795 | tintColorClassName="accent-danger-foreground/40" | |
| 796 | 796 | type="monochrome" |
| 797 | 797 | /> |
| 798 | 798 | </View> |
apps/web/src/components/chat/MessagesTimeline.test.tsx+1-1| ... | ... | @@ -1471,7 +1471,7 @@ describe("MessagesTimeline", () => { |
| 1471 | 1471 | ); |
| 1472 | 1472 | |
| 1473 | 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 | 1475 | expect(markup).not.toContain("text-destructive"); |
| 1476 | 1476 | }); |
| 1477 | 1477 |
apps/web/src/components/chat/MessagesTimeline.tsx+43-29| ... | ... | @@ -1893,6 +1893,17 @@ function ActivityShimmerOverlay({ children }: { children: ReactNode }) { |
| 1893 | 1893 | ); |
| 1894 | 1894 | } |
| 1895 | 1895 | |
| 1896 | const 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. */ | |
| 1900 | function toolIconAcceptsTint( | |
| 1901 | iconName: WorkEntryIconName, | |
| 1902 | toolIcon: ToolActivityIcon | undefined, | |
| 1903 | ): boolean { | |
| 1904 | return toolIcon === undefined && iconName !== "computer"; | |
| 1905 | } | |
| 1906 | ||
| 1896 | 1907 | function LiveActivityRow({ |
| 1897 | 1908 | label, |
| 1898 | 1909 | iconName, |
| ... | ... | @@ -1941,37 +1952,41 @@ function LiveActivityContent({ |
| 1941 | 1952 | announceFailure?: boolean; |
| 1942 | 1953 | highlighted?: boolean; |
| 1943 | 1954 | }) { |
| 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); | |
| 1947 | 1957 | |
| 1948 | 1958 | return ( |
| 1949 | 1959 | <span |
| 1950 | 1960 | className={cn( |
| 1951 | 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 | 1963 | highlighted ? "text-foreground" : "text-secondary-label", |
| 1954 | 1964 | )} |
| 1955 | 1965 | > |
| 1956 | {resolvedIconName ? ( | |
| 1966 | {iconName ? ( | |
| 1957 | 1967 | <span |
| 1958 | 1968 | className={cn( |
| 1959 | 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 | 1972 | role={announceFailure ? "img" : undefined} |
| 1963 | 1973 | aria-label={announceFailure ? "Tool call failed" : undefined} |
| 1964 | 1974 | > |
| 1965 | 1975 | <ToolActivityIconView |
| 1966 | icon={failed && !isSpecialToolIcon ? undefined : toolIcon} | |
| 1967 | fallbackName={resolvedIconName} | |
| 1976 | icon={toolIcon} | |
| 1977 | fallbackName={iconName} | |
| 1968 | 1978 | className="block size-4 shrink-0 stroke-[1.8]" |
| 1969 | 1979 | muted={!highlighted} |
| 1970 | 1980 | /> |
| 1971 | 1981 | </span> |
| 1972 | 1982 | ) : null} |
| 1973 | 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 | 1990 | </span> |
| 1976 | 1991 | ); |
| 1977 | 1992 | } |
| ... | ... | @@ -3088,12 +3103,15 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 3088 | 3103 | const iconConfig = workToneIcon(workEntry.tone); |
| 3089 | 3104 | const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning"; |
| 3090 | 3105 | 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)); | |
| 3093 | 3109 | 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); | |
| 3097 | 3115 | const previewText = displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot); |
| 3098 | 3116 | const viewedImagePath = workEntryViewedImagePath(workEntry); |
| 3099 | 3117 | const viewedImage = |
| ... | ... | @@ -3121,20 +3139,18 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 3121 | 3139 | viewedImage ? viewedImagePath : null, |
| 3122 | 3140 | ) |
| 3123 | 3141 | : 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. | |
| 3129 | 3143 | const iconWrapperClass = cn( |
| 3130 | 3144 | "flex size-6 shrink-0 items-center justify-center", |
| 3131 | 3145 | showWarningIndicator |
| 3132 | 3146 | ? "text-warning" |
| 3133 | 3147 | : showDestructiveRowStyle |
| 3134 | 3148 | ? "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, | |
| 3138 | 3154 | ); |
| 3139 | 3155 | const headingClass = showWarningIndicator |
| 3140 | 3156 | ? "font-medium text-warning" |
| ... | ... | @@ -3179,11 +3195,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 3179 | 3195 | aria-label={showFailedIndicator ? "Tool call failed" : undefined} |
| 3180 | 3196 | > |
| 3181 | 3197 | <ToolActivityIconView |
| 3182 | icon={ | |
| 3183 | showWarningIndicator || (showFailedIndicator && !hasSpecialToolIcon) | |
| 3184 | ? undefined | |
| 3185 | : (workEntry.toolIcon ?? workEntry.toolSource?.icon) | |
| 3186 | } | |
| 3198 | icon={entryToolIcon} | |
| 3187 | 3199 | fallbackName={entryIconName} |
| 3188 | 3200 | className="block size-4 shrink-0 stroke-[1.8]" |
| 3189 | 3201 | muted |
| ... | ... | @@ -3207,8 +3219,10 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 3207 | 3219 | </span> |
| 3208 | 3220 | </p> |
| 3209 | 3221 | </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)} /> | |
| 3212 | 3226 | ) : null} |
| 3213 | 3227 | <span |
| 3214 | 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 | 154 | --color-warning: var(--warning); |
| 155 | 155 | --color-warning-surface: var(--warning-surface); |
| 156 | 156 | --color-error-foreground: var(--error-foreground); |
| 157 | --color-tool-error-icon: var(--tool-error-icon); | |
| 157 | 158 | --color-error: var(--error); |
| 158 | 159 | --color-error-surface: var(--error-surface); |
| 159 | 160 | --color-update-foreground: var(--update-foreground); |
| ... | ... | @@ -948,6 +949,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 948 | 949 | --accent-foreground: var(--color-zinc-900); |
| 949 | 950 | --error: var(--color-red-500); |
| 950 | 951 | --error-foreground: var(--color-red-700); |
| 952 | --tool-error-icon: var(--error); | |
| 951 | 953 | --error-surface: color-mix(in srgb, var(--error) 8%, transparent); |
| 952 | 954 | --destructive: var(--error); |
| 953 | 955 | --border: var(--color-zinc-200); |
| ... | ... | @@ -1002,6 +1004,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1002 | 1004 | --accent-foreground: var(--color-neutral-100); |
| 1003 | 1005 | --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white)); |
| 1004 | 1006 | --error-foreground: var(--color-red-400); |
| 1007 | --tool-error-icon: #fca5a5; | |
| 1005 | 1008 | --error-surface: color-mix(in srgb, var(--error) 16%, transparent); |
| 1006 | 1009 | --border: --alpha(var(--color-white) / 6%); |
| 1007 | 1010 | --input: --alpha(var(--color-white) / 8%); |
| ... | ... | @@ -1114,6 +1117,7 @@ html[data-theme-id]:not([data-theme-id=""]) { |
| 1114 | 1117 | --message-action-hover: var(--app-theme-message-action-hover); |
| 1115 | 1118 | --error: var(--app-theme-error); |
| 1116 | 1119 | --error-foreground: var(--app-theme-error-foreground); |
| 1120 | --tool-error-icon: var(--error-foreground); | |
| 1117 | 1121 | --error-surface: var(--app-theme-error-surface); |
| 1118 | 1122 | --destructive: var(--error); |
| 1119 | 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 | 13 | Expanded rows follow the call's own state, such as "Clicked" after success. |
| 14 | 14 | When a call has not reported a state yet, the label stays in the present tense. |
| 15 | 15 | Failed, declined, and stopped calls say what happened without implying success. |
| 16 | Failed calls keep their tool icon with a muted red tint, so a failed command still | |
| 17 | looks like a command. App logos and other multicolor icons keep their artwork and | |
| 18 | show a small muted red failure mark beside the row. Expand the call to inspect its | |
| 19 | output. Runtime errors and warnings keep their stronger styling. | |
| 16 | 20 | Preview browser actions use a globe icon. Other T3 tools keep the T3 mark. |
| 17 | 21 | Group summaries count browser actions separately, such as "Used browser 18 times" or |
| 18 | 22 | "Ran 4 commands and used browser 15 times". Browser-only groups also use a globe icon. |