Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 21 additions & 12 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ import {
} from "./thread-feed-live-follow";
import {
collapsedWorkLogHeight,
ThreadAgentSpawnCard,
ThreadDisclosureChevron,
ThreadWorkGroupToggle,
ThreadThinkingRow,
Expand Down Expand Up @@ -504,12 +505,7 @@ function MessageAttachmentFile(props: {
function MessageAttachmentUnknown(props: { readonly name: string }) {
return (
<View className="flex-row items-center gap-2 py-1">
<SymbolView
name="doc.text"
size={16}
tintColorClassName="accent-icon-subtle"
type="monochrome"
/>
<SymbolView name="doc.text" size={16} tintColor="#a3a3a3" type="monochrome" />
<Text className="min-w-0 flex-1 text-sm text-foreground" numberOfLines={1}>
{props.name}
</Text>
Expand Down Expand Up @@ -1357,7 +1353,7 @@ function renderFeedEntry(
accessibilityState={{ expanded: entry.expanded }}
onPress={() => props.onToggleTurnFold(entry.turnId)}
hitSlop={4}
className="mb-1 min-h-11 flex-row items-center gap-2 border-b border-border px-2"
className="mb-1 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2"
style={{
minHeight: Math.max(TURN_FOLD_HEIGHT - 3.5, props.workRowSizing.estimatedRowHeight),
}}
Expand All @@ -1382,6 +1378,19 @@ function renderFeedEntry(
return <ThreadThinkingRow rowSizing={props.workRowSizing} iconSubtleColor={iconSubtleColor} />;
}

if (entry.type === "agent-spawn") {
return (
<ThreadAgentSpawnCard
summary={entry.summary}
expanded={entry.expanded}
iconSubtleColor={iconSubtleColor}
rowSizing={props.workRowSizing}
onToggle={() => props.onToggleWorkGroup(entry.id, entry.id)}
onCopy={() => props.onCopyWorkRow(entry.activity.id, entry.activity.getCopyText())}
/>
);
}

if (entry.type === "work-toggle") {
return (
<ThreadWorkGroupToggle
Expand Down Expand Up @@ -1411,7 +1420,7 @@ function renderFeedEntry(
accessibilityLabel={label}
className="mb-3 flex-row items-center gap-3 px-1 py-1"
>
<View className="h-px flex-1 bg-border" />
<View className="h-px flex-1 bg-adaptive-neutral-200-a80-white-a8" />
<View className="shrink-0 flex-row items-center gap-1.5">
<SymbolView
name="arrow.down.right.and.arrow.up.left"
Expand All @@ -1421,7 +1430,7 @@ function renderFeedEntry(
/>
<Text className="font-t3-medium text-xs text-foreground-muted">{label}</Text>
</View>
<View className="h-px flex-1 bg-border" />
<View className="h-px flex-1 bg-adaptive-neutral-200-a80-white-a8" />
</View>
);
}
Expand Down Expand Up @@ -1508,7 +1517,7 @@ function renderFeedEntry(
})}
</View>
<View className="mt-1 flex-row items-center justify-end gap-1 pr-0.5">
<Text className="font-t3-medium text-xs tabular-nums text-foreground-secondary">
<Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400">
{timestampLabel}
</Text>
{message.text.trim().length > 0 ? (
Expand Down Expand Up @@ -1557,7 +1566,7 @@ function renderFeedEntry(
attachmentId={attachment.id}
name={attachment.name}
mimeType={attachment.mimeType}
className="mt-1.5 aspect-[1.3] w-full rounded-[18px] bg-subtle-strong"
className="mt-1.5 aspect-[1.3] w-full rounded-[18px] bg-adaptive-neutral-200-800"
onPressPreview={props.onPressPreview}
/>
) : isFileAttachment(attachment) ? (
Expand All @@ -1581,7 +1590,7 @@ function renderFeedEntry(
buttonSize={28}
iconSize={13}
/>
<Text className="font-t3-medium text-xs tabular-nums text-foreground-secondary">
<Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400">
{timestampLabel}
</Text>
</View>
Expand Down
195 changes: 172 additions & 23 deletions apps/mobile/src/features/threads/thread-work-log.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ import { AppText as Text } from "../../components/AppText";
import { T3Wordmark } from "../../components/T3Wordmark";
import { cn } from "../../lib/cn";
import { THREAD_WORK_ROW_MIN_HEIGHT, type deriveThreadWorkLogSizing } from "../../lib/layout";
import { type ThreadFeedActivity, workEntryRowLabel } from "../../lib/threadActivity";
import {
type AgentSpawnSummary,
type ThreadFeedActivity,
workEntryRowLabel,
} from "../../lib/threadActivity";
import {
resolveThreadWorkGroupInitialScroll,
shouldFollowThreadWorkGroupAppend,
Expand Down Expand Up @@ -135,6 +139,7 @@ export function ThreadDisclosureChevron(props: {
}

function ShimmerWorkContent(props: {
readonly compact?: boolean;
readonly environmentId?: EnvironmentId;
readonly highlighted: boolean;
readonly icon: WorkContentIcon;
Expand All @@ -147,26 +152,29 @@ function ShimmerWorkContent(props: {
}) {
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.toolIcon && props.environmentId ? (
<ToolActivityIconView
environmentId={props.environmentId}
icon={props.toolIcon}
fallback={props.icon}
fallbackColor={props.iconSubtleColor}
themeAppearance={props.themeAppearance ?? "light"}
/>
) : props.showIcon ? (
<WorkLogIcon
icon={props.icon}
color={props.iconSubtleColor}
highlighted={props.highlighted}
/>
) : null}
</View>
{props.showIcon ? (
<View className="h-6 w-6 shrink-0 items-center justify-center">
{props.toolIcon && props.environmentId ? (
<ToolActivityIconView
environmentId={props.environmentId}
icon={props.toolIcon}
fallback={props.icon}
fallbackColor={props.iconSubtleColor}
themeAppearance={props.themeAppearance ?? "light"}
/>
) : (
<WorkLogIcon
icon={props.icon}
color={props.iconSubtleColor}
highlighted={props.highlighted}
/>
)}
</View>
) : null}
<Text
className={cn(
"min-w-0 shrink text-sm",
"min-w-0 shrink",
props.compact ? "text-xs" : "text-sm",
props.highlighted ? "text-foreground" : "text-foreground-muted",
)}
numberOfLines={1}
Expand All @@ -179,6 +187,8 @@ function ShimmerWorkContent(props: {
}

export function ShimmeringWorkContent(props: {
/** Secondary line: no icon slot, caption size. */
readonly compact?: boolean;
readonly environmentId?: EnvironmentId;
readonly icon: WorkContentIcon;
readonly iconSubtleColor: ColorValue;
Expand All @@ -194,7 +204,10 @@ export function ShimmeringWorkContent(props: {
const screenIsFocused = useIsFocused();
const progress = useSharedValue(0);
const gradientId = `work-shimmer-${useId().replaceAll(":", "")}`;
const contentWidth = Math.min(availableWidth, SHIMMER_ICON_AND_GAP_WIDTH + Math.ceil(textWidth));
const contentWidth = Math.min(
availableWidth,
(props.showIcon ? SHIMMER_ICON_AND_GAP_WIDTH : 0) + Math.ceil(textWidth),
);

useEffect(() => {
const subscription = AppState.addEventListener("change", (state) => {
Expand Down Expand Up @@ -250,6 +263,7 @@ export function ShimmeringWorkContent(props: {
onLayout={(event) => setAvailableWidth(event.nativeEvent.layout.width)}
>
<ShimmerWorkContent
compact={props.compact}
environmentId={props.environmentId}
highlighted={false}
icon={props.icon}
Expand Down Expand Up @@ -290,6 +304,7 @@ export function ShimmeringWorkContent(props: {
>
<Animated.View style={[{ width: availableWidth }, counterSweepStyle]}>
<ShimmerWorkContent
compact={props.compact}
environmentId={props.environmentId}
highlighted
icon={props.icon}
Expand Down Expand Up @@ -773,7 +788,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
color={props.iconSubtleColor}
colorClassName={
iconIsDestructive
? "accent-danger-foreground"
? "accent-adaptive-rose-600-400"
: failed
? "accent-danger-foreground/40"
: undefined
Expand All @@ -784,7 +799,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
<Text
className={cn(
"min-w-0 flex-1 text-sm text-foreground-muted",
iconIsDestructive && "font-t3-medium text-danger-foreground",
iconIsDestructive && "font-t3-medium text-adaptive-rose-600-400",
)}
numberOfLines={1}
>
Expand Down Expand Up @@ -832,7 +847,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
entering={WORK_LOG_DETAIL_ENTER_TRANSITION}
exiting={WORK_LOG_DETAIL_EXIT_TRANSITION}
layout={WORK_LOG_LAYOUT_TRANSITION}
className="ml-7 border-l border-border pb-1 pl-3 pt-0.5"
className="ml-7 border-l border-adaptive-neutral-300-a60-white-a12 pb-1 pl-3 pt-0.5"
>
{viewedImagePath ? (
<View className="pb-1.5">
Expand Down Expand Up @@ -938,6 +953,140 @@ export function ThreadWorkGroupToggle(props: {
);
}

const AGENT_SPAWN_TONE_DOT_CLASS = {
working: "bg-adaptive-sky-600-400",
completed: "bg-adaptive-emerald-600-400",
failed: "bg-adaptive-rose-600-400",
stopped: "bg-foreground-muted",
} as const satisfies Record<AgentSpawnSummary["tone"], string>;

/**
* A batch of spawned subagents. The status line updates in place as members
* report progress; expanding lists each member. Text nodes carry keys tied to
* the row identity only, so a progress tick re-renders the labels without
* remounting the card (see the batch key in appendActivityGroupRows).
*/
export const ThreadAgentSpawnCard = memo(function ThreadAgentSpawnCard(props: {
readonly summary: AgentSpawnSummary;
readonly expanded: boolean;
readonly iconSubtleColor: ColorValue;
readonly rowSizing: ReturnType<typeof deriveThreadWorkLogSizing>;
readonly onToggle: () => void;
readonly onCopy: () => void;
}) {
const { summary, expanded } = props;
const working = summary.tone === "working";
const memberCount = summary.members.length;
const canExpand = memberCount > 0;
return (
<Animated.View layout={WORK_LOG_LAYOUT_TRANSITION} className="-mx-1 mb-1 px-1">
<Pressable
accessibilityRole={canExpand ? "button" : undefined}
accessibilityState={canExpand ? { expanded } : undefined}
accessibilityLabel={`${summary.title}, ${summary.status}`}
accessibilityHint={
canExpand
? `Double tap to ${expanded ? "hide" : "show"} ${memberCount} ${memberCount === 1 ? "subagent" : "subagents"}. Long press to copy.`
: "Long press to copy."
}
hitSlop={4}
onPress={() => {
if (!canExpand) return;
void Haptics.selectionAsync();
props.onToggle();
}}
onLongPress={props.onCopy}
className="rounded-xl border border-adaptive-neutral-200-a80-white-a8 bg-card px-2.5 py-2 active:bg-subtle"
>
<View className="flex-row items-center gap-2">
<View className="h-6 w-6 shrink-0 items-center justify-center">
<SymbolView
name={{ ios: "sparkles", android: "auto_awesome" }}
size={14}
weight="medium"
tintColor={props.iconSubtleColor}
type="monochrome"
/>
</View>
<View className="min-w-0 flex-1 gap-0.5">
<Text
key={props.rowSizing.textSizeKey}
className="font-t3-medium text-sm text-foreground"
numberOfLines={1}
>
{summary.title}
</Text>
<View className="flex-row items-center gap-1.5">
<View
className={cn(
"h-1.5 w-1.5 shrink-0 rounded-full",
AGENT_SPAWN_TONE_DOT_CLASS[summary.tone],
)}
/>
{working ? (
<ShimmeringWorkContent
key={props.rowSizing.textSizeKey}
compact
icon="brain"
iconSubtleColor={props.iconSubtleColor}
label={summary.status}
showIcon={false}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
/>
) : (
<Text className="min-w-0 flex-1 text-xs text-foreground-muted" numberOfLines={1}>
{summary.status}
</Text>
)}
</View>
</View>
{canExpand ? (
<ThreadDisclosureChevron
expanded={expanded}
collapsedDirection="down"
size={11}
tintColor={props.iconSubtleColor}
/>
) : null}
</View>
{expanded && canExpand ? (
<Animated.View
entering={WORK_LOG_DETAIL_ENTER_TRANSITION}
exiting={WORK_LOG_DETAIL_EXIT_TRANSITION}
layout={WORK_LOG_LAYOUT_TRANSITION}
className="ml-8 mt-1.5 gap-1.5 border-l border-adaptive-neutral-300-a60-white-a12 pl-3"
>
{summary.members.map((member) => (
<View key={member.title} className="gap-px">
<View className="flex-row items-center gap-1.5">
<View
className={cn(
"h-1.5 w-1.5 shrink-0 rounded-full",
AGENT_SPAWN_TONE_DOT_CLASS[member.tone],
)}
/>
<Text className="min-w-0 flex-1 text-xs text-foreground" numberOfLines={1}>
{member.title}
</Text>
<Text className="shrink-0 text-2xs text-foreground-muted">{member.status}</Text>
</View>
{member.detail ? (
<Text
selectable
className="pl-3 font-mono text-2xs leading-normal text-foreground-muted"
numberOfLines={expanded ? 6 : 1}
>
{member.detail}
</Text>
) : null}
</View>
))}
</Animated.View>
) : null}
</Pressable>
</Animated.View>
);
});

export function ThreadThinkingRow(props: {
readonly rowSizing: ReturnType<typeof deriveThreadWorkLogSizing>;
readonly iconSubtleColor: ColorValue;
Expand Down
Loading
Loading