Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
513837f
feat(work-log): display native app and browser tool icons
Yash-Singh1 Sep 1, 2026
3016647
Merge remote-tracking branch 'origin/main' into t3code/label-computer…
Yash-Singh1 Sep 1, 2026
3453588
fix(work-log): address automated review findings
Yash-Singh1 Sep 2, 2026
9b27ad6
fix(server): bound native icon discovery
Yash-Singh1 Sep 2, 2026
5a0dc4a
fix(work-log): stabilize icon caches
Yash-Singh1 Sep 2, 2026
40d3eea
fix(work-log): retain refreshed native icons
Yash-Singh1 Sep 2, 2026
b64aa3b
refactor(server): scope native icon resolver state
Yash-Singh1 Sep 2, 2026
68f87a1
refactor(server): declare native icon service contract
Yash-Singh1 Sep 2, 2026
40d1741
test(server): construct native icon resolver as macOS
Yash-Singh1 Sep 2, 2026
f243cfd
Merge remote-tracking branch 'origin/main' into t3code/label-computer…
Yash-Singh1 Sep 2, 2026
3d4cf1b
style(shared): format favicon tests
Yash-Singh1 Sep 2, 2026
a2af4ad
refactor(server): make native icon lookup best effort
Yash-Singh1 Sep 2, 2026
4e676ec
fix(work-log): address remaining review findings
Yash-Singh1 Sep 2, 2026
3e4b86a
fix(work-log): retain matching legacy icon metadata
Yash-Singh1 Sep 2, 2026
b731cb5
fix(work-log): preserve explicit computer icons
Yash-Singh1 Sep 2, 2026
a5c58c8
Merge remote-tracking branch 'origin/main' into t3code/label-computer…
Yash-Singh1 Sep 2, 2026
e0f1c6e
fix(ui): format merged tool activity renderers
Yash-Singh1 Sep 2, 2026
1540313
fix(work-log): preserve legacy tool presentation edge cases
Yash-Singh1 Sep 2, 2026
f7ee08a
refactor(work-log): remove legacy tool presentation inference
Yash-Singh1 Sep 2, 2026
d1a46a0
Merge remote-tracking branch 'origin/main' into t3code/label-computer…
Yash-Singh1 Sep 2, 2026
b52aa75
fix(mobile): preserve failed tool icons
Yash-Singh1 Sep 2, 2026
2c0c9a0
fix(work-log): mute tool icons without masking detail
maria-rcks Sep 3, 2026
748ce38
chore: merge main into tool icon work log branch
maria-rcks Sep 3, 2026
9727def
fix(web): use light variant for muted tool icons
maria-rcks Sep 3, 2026
66faa80
fix(codex): bound tool presentation metadata
maria-rcks Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .tmp/dev-desktop-expanded-groups.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/mobile/src/components/AppSymbol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {
close: IconX,
construction: IconHammer,
content_copy: IconCopy,
desktop_windows: IconDeviceDesktop,
edit: IconEdit,
error: IconAlertCircle,
folder: IconFolder,
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,7 @@ function renderFeedEntry(
readonly markdownStyles: MarkdownStyleSets;
readonly reviewCommentColors: ReviewCommentColors;
readonly reviewCommentBubbleWidth: number;
readonly themeAppearance: "light" | "dark";
readonly userBubbleMaxWidth: number;
},
) {
Expand Down Expand Up @@ -1369,11 +1370,15 @@ function renderFeedEntry(
if (entry.type === "work-toggle") {
return (
<ThreadWorkGroupToggle
environmentId={props.environmentId}
expanded={entry.expanded}
hiddenCount={entry.hiddenCount}
iconSubtleColor={iconSubtleColor}
summary={entry.summary}
summaryKind={entry.summaryKind}
themeAppearance={props.themeAppearance}
toolSurface={entry.toolSurface}
toolIcon={entry.toolIcon}
hasFailure={entry.hasFailure}
shimmer={entry.shimmer}
onToggle={() => props.onToggleWorkGroup(entry.groupId)}
Expand Down Expand Up @@ -1540,9 +1545,11 @@ function renderFeedEntry(
return (
<ThreadWorkLog
activities={entry.activities}
environmentId={props.environmentId}
copiedRowId={props.copiedRowId}
expandedRows={props.expandedWorkRows}
iconSubtleColor={iconSubtleColor}
themeAppearance={props.themeAppearance}
onCopyRow={props.onCopyWorkRow}
onToggleRow={props.onToggleWorkRow}
/>
Expand Down Expand Up @@ -1821,6 +1828,7 @@ function ThreadFeedPlaceholder(props: {

export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
const navigation = useNavigation();
const { themeAppearance } = useAppearancePreferences();
const copyFeedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const disclosureSettleFrameRef = useRef<number | null>(null);
const disclosureSettleSecondFrameRef = useRef<number | null>(null);
Expand Down Expand Up @@ -2448,6 +2456,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
markdownStyles,
reviewCommentColors,
reviewCommentBubbleWidth,
themeAppearance,
userBubbleMaxWidth,
skills: props.skills,
onUseArtifactTemplate: props.onUseArtifactTemplate,
Expand All @@ -2465,6 +2474,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
markdownStyles,
reviewCommentColors,
reviewCommentBubbleWidth,
themeAppearance,
userBubbleMaxWidth,
onCopyWorkRow,
onMarkdownLinkPress,
Expand Down
172 changes: 156 additions & 16 deletions apps/mobile/src/features/threads/thread-work-log.tsx
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Haptics from "expo-haptics";
import { Image } from "expo-image";
import { type AppSymbolName, SymbolView } from "../../components/AppSymbol";
import { MaskedView } from "@expo/ui/community/masked-view";
import { useIsFocused } from "@react-navigation/native";
Expand All @@ -13,6 +14,8 @@ import {
View,
} from "react-native";
import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
import type { EnvironmentId, ToolActivityIcon } from "@t3tools/contracts";
import { toolActivityFaviconUrl } from "@t3tools/shared/favicon";

import { AppText as Text } from "../../components/AppText";
import { cn } from "../../lib/cn";
Expand All @@ -32,6 +35,7 @@ import Animated, {
withSequence,
withTiming,
} from "react-native-reanimated";
import { useAssetUrl } from "../../state/assets";

const SHIMMER_WIDTH = 72;
const SHIMMER_SWEEP_MS = 1_350;
Expand All @@ -43,17 +47,28 @@ const WORK_LOG_DETAIL_ENTER_TRANSITION = FadeIn.duration(140);
const WORK_LOG_DETAIL_EXIT_TRANSITION = FadeOut.duration(120);

function ShimmerWorkContent(props: {
readonly environmentId?: EnvironmentId;
readonly highlighted: boolean;
readonly icon: AppSymbolName;
readonly iconSubtleColor: ColorValue;
readonly label: string;
readonly onTextLayout?: ComponentProps<typeof Text>["onTextLayout"];
readonly showIcon: boolean;
readonly themeAppearance?: "light" | "dark";
readonly toolIcon?: ToolActivityIcon;
}) {
return (
<View className="flex-row items-center gap-1.5">
<View className="h-6 w-6 shrink-0 items-center justify-center">
{props.showIcon ? (
{props.showIcon && props.toolIcon && props.environmentId ? (
<ToolActivityIconView
environmentId={props.environmentId}
icon={props.toolIcon}
fallback={props.icon}
fallbackColor={props.iconSubtleColor}
themeAppearance={props.themeAppearance ?? "light"}
/>
) : props.showIcon ? (
<SymbolView
name={props.icon}
size={14}
Expand All @@ -80,10 +95,13 @@ function ShimmerWorkContent(props: {
}

export function ShimmeringWorkContent(props: {
readonly environmentId?: EnvironmentId;
readonly icon: AppSymbolName;
readonly iconSubtleColor: ColorValue;
readonly label: string;
readonly showIcon: boolean;
readonly themeAppearance?: "light" | "dark";
readonly toolIcon?: ToolActivityIcon;
}) {
const [availableWidth, setAvailableWidth] = useState(0);
const [textWidth, setTextWidth] = useState(0);
Expand Down Expand Up @@ -148,11 +166,14 @@ export function ShimmeringWorkContent(props: {
onLayout={(event) => setAvailableWidth(event.nativeEvent.layout.width)}
>
<ShimmerWorkContent
environmentId={props.environmentId}
highlighted={false}
icon={props.icon}
iconSubtleColor={props.iconSubtleColor}
label={props.label}
showIcon={props.showIcon}
themeAppearance={props.themeAppearance}
toolIcon={props.toolIcon}
onTextLayout={(event) => setTextWidth(event.nativeEvent.lines[0]?.width ?? 0)}
/>
{!reducedMotion && appIsActive && screenIsFocused && contentWidth > 0 ? (
Expand Down Expand Up @@ -185,11 +206,14 @@ export function ShimmeringWorkContent(props: {
>
<Animated.View style={[{ width: availableWidth }, counterSweepStyle]}>
<ShimmerWorkContent
environmentId={props.environmentId}
highlighted
icon={props.icon}
iconSubtleColor={props.iconSubtleColor}
label={props.label}
showIcon={props.showIcon}
themeAppearance={props.themeAppearance}
toolIcon={props.toolIcon}
/>
</Animated.View>
</MaskedView>
Expand Down Expand Up @@ -220,10 +244,14 @@ function workRowSymbolName(icon: ThreadFeedActivity["icon"]): AppSymbolName {
return { ios: "sparkles", android: "auto_awesome" };
case "alert":
return { ios: "exclamationmark.triangle", android: "error" };
case "browser":
return { ios: "safari", android: "public" };
case "check":
return { ios: "checkmark", android: "check" };
case "command":
return { ios: "terminal", android: "terminal" };
case "computer":
return { ios: "desktopcomputer", android: "desktop_windows" };
case "edit":
return { ios: "square.and.pencil", android: "edit" };
case "eye":
Expand Down Expand Up @@ -272,9 +300,11 @@ export function collapsedWorkLogHeight(activities: ReadonlyArray<ThreadFeedActiv

export function ThreadWorkLog(props: {
readonly activities: ReadonlyArray<ThreadFeedActivity>;
readonly environmentId: EnvironmentId;
readonly copiedRowId: string | null;
readonly expandedRows: Readonly<Record<string, boolean>>;
readonly iconSubtleColor: import("react-native").ColorValue;
readonly themeAppearance: "light" | "dark";
readonly onCopyRow: (rowId: string, value: string) => void;
readonly onToggleRow: (rowId: string) => void;
}) {
Expand All @@ -297,7 +327,6 @@ export function ThreadWorkLog(props: {
const displayText = row.detail ?? row.summary;
const iconIsDestructive = row.icon === "alert" || row.icon === "warning";
const failed = row.status === "failure";
const showIcon = !row.groupedToolDetail || iconIsDestructive || failed;

return (
<Animated.View
Expand Down Expand Up @@ -328,27 +357,34 @@ export function ThreadWorkLog(props: {
<View className="min-h-8 flex-row items-center gap-1.5">
{row.live ? (
<ShimmeringWorkContent
environmentId={props.environmentId}
icon={workRowSymbolName(row.icon)}
iconSubtleColor={props.iconSubtleColor}
label={displayText}
showIcon={showIcon}
showIcon
themeAppearance={props.themeAppearance}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
toolIcon={row.workEntry.toolIcon ?? row.workEntry.toolSource?.icon}
/>
) : (
<>
<View className="h-6 w-6 shrink-0 items-center justify-center">
{showIcon ? (
{failed ? (
<SymbolView
name={
failed
? { ios: "xmark", android: "close" }
: workRowSymbolName(row.icon)
}
name={{ ios: "xmark", android: "close" }}
size={14}
weight="medium"
tintColor={iconIsDestructive ? "#e11d48" : props.iconSubtleColor}
type="monochrome"
/>
) : null}
) : (
<ToolActivityIconView
environmentId={props.environmentId}
icon={row.workEntry.toolIcon ?? row.workEntry.toolSource?.icon}
fallback={workRowSymbolName(row.icon)}
fallbackColor={props.iconSubtleColor}
themeAppearance={props.themeAppearance}
/>
)}
</View>
<Text
className={cn(
Expand Down Expand Up @@ -418,19 +454,25 @@ export function ThreadWorkLog(props: {
}

export function ThreadWorkGroupToggle(props: {
readonly environmentId: EnvironmentId;
readonly expanded: boolean;
readonly hiddenCount: number;
readonly iconSubtleColor: import("react-native").ColorValue;
readonly summary: string;
readonly summaryKind: ToolGroupSummaryKind;
readonly toolSurface?: import("@t3tools/contracts").ToolActivitySurface;
readonly toolIcon?: ToolActivityIcon;
readonly themeAppearance: "light" | "dark";
readonly hasFailure: boolean;
readonly shimmer: boolean;
readonly onToggle: () => void;
}) {
const accessibilityLabel = props.hasFailure
? `${props.summary}, tool call failed`
: props.summary;
const icon = toolGroupSummarySymbolName(props.summaryKind);
const icon = props.toolSurface
? workRowSymbolName(props.toolSurface)
: toolGroupSummarySymbolName(props.summaryKind);

return (
<View className="-mx-1 mb-1 px-1 py-0">
Expand All @@ -456,11 +498,12 @@ export function ThreadWorkGroupToggle(props: {
) : (
<>
<View className="h-6 w-6 items-center justify-center">
<SymbolView
name={icon}
size={14}
tintColor={props.iconSubtleColor}
type="monochrome"
<ToolActivityIconView
Comment thread
Yash-Singh1 marked this conversation as resolved.
environmentId={props.environmentId}
icon={props.toolIcon}
fallback={icon}
fallbackColor={props.iconSubtleColor}
themeAppearance={props.themeAppearance}
/>
</View>
<Text className="min-w-0 flex-1 text-sm text-foreground-muted" numberOfLines={1}>
Expand All @@ -483,6 +526,103 @@ export function ThreadWorkGroupToggle(props: {
);
}

function ToolActivityIconView(props: {
readonly environmentId: EnvironmentId;
readonly icon?: ToolActivityIcon;
readonly fallback: AppSymbolName;
readonly fallbackColor: ColorValue;
readonly themeAppearance: "light" | "dark";
}) {
if (!props.icon) {
return <ToolActivityIconFallback name={props.fallback} color={props.fallbackColor} />;
}
if (props.icon._tag === "website") {
const source = toolActivityFaviconUrl(props.icon, props.themeAppearance, 32);
return source ? (
<ToolActivityImage
key={source}
source={source}
fallback={props.fallback}
color={props.fallbackColor}
/>
) : (
<ToolActivityIconFallback name={props.fallback} color={props.fallbackColor} />
);
}
if (props.icon._tag === "themed-logo") {
const source =
props.themeAppearance === "dark"
? (props.icon.logoUrlDark ?? props.icon.logoUrl)
: props.icon.logoUrl;
return (
<ToolActivityImage
key={source}
source={source}
fallback={props.fallback}
color={props.fallbackColor}
/>
);
}
return (
<NativeAppToolActivityIcon
environmentId={props.environmentId}
app={props.icon.app}
fallback={props.fallback}
color={props.fallbackColor}
/>
);
}

function NativeAppToolActivityIcon(props: {
readonly environmentId: EnvironmentId;
readonly app: Extract<ToolActivityIcon, { readonly _tag: "native-app" }>["app"];
readonly fallback: AppSymbolName;
readonly color: ColorValue;
}) {
const source = useAssetUrl(props.environmentId, {
_tag: "native-app-icon",
app: props.app,
});
return source ? (
<ToolActivityImage key={source} source={source} fallback={props.fallback} color={props.color} />
) : (
<ToolActivityIconFallback name={props.fallback} color={props.color} />
);
}

function ToolActivityImage(props: {
readonly source: string;
readonly fallback: AppSymbolName;
readonly color: ColorValue;
}) {
const [loaded, setLoaded] = useState(false);
const [failed, setFailed] = useState(false);
return (
<View className="h-4 w-4 items-center justify-center overflow-hidden rounded-[3px] bg-background">
{!loaded || failed ? (
<ToolActivityIconFallback name={props.fallback} color={props.color} />
) : null}
{!failed ? (
<Image
source={props.source}
cachePolicy="memory-disk"
contentFit="contain"
style={[StyleSheet.absoluteFill, { opacity: loaded ? 1 : 0 }]}
onLoad={() => setLoaded(true)}
onError={() => setFailed(true)}
/>
) : null}
</View>
);
}

function ToolActivityIconFallback(props: {
readonly name: AppSymbolName;
readonly color: ColorValue;
}) {
return <SymbolView name={props.name} size={14} tintColor={props.color} type="monochrome" />;
}

function toolGroupSummarySymbolName(kind: ToolGroupSummaryKind): AppSymbolName {
switch (kind) {
case "read":
Expand Down
Loading
Loading