Open History to see the last commit
1import { useAtomValue } from "@effect/atom-react";
2import { StackActions, useNavigation } from "@react-navigation/native";
3import { scopeThreadRef } from "@t3tools/client-runtime/environment";
4import {
5 resolveProviderInstanceDisplayName,
6 shouldShowInstanceBadge,
7} from "@t3tools/client-runtime/state/provider-instance-display";
8import { summarizeSubagentStatuses } from "@t3tools/client-runtime/state/subagent-display";
9import { isActiveSubagentStatus } from "@t3tools/client-runtime/state/subagentRuntime";
10import type {
11 EnvironmentId,
12 OrchestrationV2Subagent,
13 OrchestrationV2TurnItem,
14 ServerConfig,
15} from "@t3tools/contracts";
16import { Pressable, View, type ColorValue } from "react-native";
17
18import { AppText as Text } from "../../components/ui/AppText";
19import { SymbolView } from "../../components/ui/AppSymbol";
20import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIcon";
21import { cn } from "../../lib/cn";
22import type { ThreadFeedActivity } from "../../lib/threadActivity";
23import { serverEnvironment } from "../../state/server";
24import { environmentThreadDetails } from "../../state/threads";
25import { subagentCardElapsed } from "./subagent-card-presentation";
26import { useEnvironments } from "../../state/environments";
27import { useServerConfigs } from "../../state/entities";
28import { SubagentRow } from "./SubagentRow";
29import { useVisibleSecondClock } from "./use-visible-second-clock";
30import { subagentThreadTarget } from "./threadAgentsPresentation";
31import { WorkLogBlock } from "./work-log-layout";
32
33import { useOpenThreadInspector } from "./thread-inspector-navigation";
34
35type SubagentItem = Extract<OrchestrationV2TurnItem, { type: "subagent" }>;
36type AgentTiming = Pick<OrchestrationV2Subagent, "status" | "startedAt" | "completedAt">;
37
38function SubagentElapsed({ agents }: { readonly agents: ReadonlyArray<AgentTiming> }) {
39 const live = agents.some((agent) => isActiveSubagentStatus(agent.status));
40 const nowMs = useVisibleSecondClock(live);
41 const elapsed = subagentCardElapsed(agents, nowMs);
42 return elapsed ? (
43 <Text className="shrink-0 text-xs tabular-nums text-foreground-muted">{elapsed}</Text>
44 ) : null;
45}
46
47function SubagentAvatar(props: {
48 readonly item: SubagentItem;
49 readonly config: ServerConfig | null | undefined;
50}) {
51 const providers = props.config?.providers ?? [];
52 const provider = providers.find(
53 (candidate) => candidate.instanceId === props.item.providerInstanceId,
54 );
55 // A remote task's own driver is only a "remote" label.
56 const driver =
57 provider?.driver ?? (props.item.remoteChild === undefined ? props.item.driver : undefined);
58 const badgeEntries = providers.map((candidate) => {
59 const rawConfig = props.config?.settings.providerInstances[candidate.instanceId]?.config;
60 const instanceConfig =
61 rawConfig !== null && typeof rawConfig === "object"
62 ? (rawConfig as Readonly<Record<string, unknown>>)
63 : null;
64 const agentId = instanceConfig?.source === "local" ? undefined : instanceConfig?.agentId;
65 return {
66 ...candidate,
67 driverKind: candidate.driver,
68 acpRegistryAgentId: typeof agentId === "string" ? agentId.trim() || undefined : undefined,
69 };
70 });
71 const badgeEntry = badgeEntries.find(
72 (candidate) => candidate.instanceId === props.item.providerInstanceId,
73 );
74 const showBadge =
75 provider !== undefined &&
76 badgeEntry !== undefined &&
77 shouldShowInstanceBadge(badgeEntry, badgeEntries);
78 return (
79 <View
80 accessible={false}
81 className="h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border bg-card"
82 >
83 {showBadge ? (
84 <ProviderInstanceIcon
85 provider={provider.driver}
86 iconUrl={provider.iconUrl}
87 size={15}
88 displayName={resolveProviderInstanceDisplayName(provider)}
89 accentColor={provider.accentColor}
90 showBadge
91 />
92 ) : driver === undefined ? (
93 <SymbolView name="robot" size={13} tintColorClassName="accent-icon-muted" />
94 ) : (
95 <ProviderIcon provider={driver} iconUrl={provider?.iconUrl} size={15} />
96 )}
97 </View>
98 );
99}
100
101export function ThreadSubagentGroup(props: {
102 readonly activities: ReadonlyArray<ThreadFeedActivity>;
103 readonly environmentId: EnvironmentId;
104 readonly anchorKey: string;
105 readonly expandedRows: Readonly<Record<string, boolean>>;
106 readonly iconSubtleColor: ColorValue;
107 readonly onToggleRow: (rowId: string, anchorKey: string) => void;
108}) {
109 const config = useAtomValue(serverEnvironment.configValueAtom(props.environmentId));
110 const navigation = useNavigation();
111 const openInspector = useOpenThreadInspector();
112 const members = props.activities.flatMap(({ projectedItem }) =>
113 projectedItem.item.type === "subagent" ? [projectedItem.item] : [],
114 );
115 const liveAgents = useAtomValue(
116 environmentThreadDetails.threadAtom(scopeThreadRef(props.environmentId, members[0]!.threadId)),
117 (thread) => thread?.projection.subagents,
118 );
119 const { presentationById } = useEnvironments();
120 const agents = members.map((item) => {
121 const live = liveAgents?.find((agent) => agent.id === item.subagentId);
122 return {
123 ...item,
124 item,
125 status: live?.status ?? item.status,
126 startedAt: live?.startedAt ?? item.startedAt,
127 completedAt: live?.completedAt ?? item.completedAt,
128 result: live?.result ?? item.result,
129 progress: live?.progress ?? item.progress,
130 model: live?.model ?? null,
131 modelSelection: live?.modelSelection,
132 };
133 });
134 const grouped = agents.length > 1;
135 const label = `${agents.length} subagents`;
136 const summary = summarizeSubagentStatuses(agents.map((agent) => agent.status));
137 const expanded = props.expandedRows[props.anchorKey] ?? false;
138 const serverConfigs = useServerConfigs();
139 // A task in a linked environment runs a provider instance of that environment.
140 const configOf = (item: SubagentItem) =>
141 item.remoteChild === undefined ? config : serverConfigs.get(item.remoteChild.environmentId);
142 return (
143 <WorkLogBlock>
144 {grouped ? (
145 <Pressable
146 accessibilityRole="button"
147 accessibilityLabel={`${label}, ${summary}`}
148 accessibilityState={{ expanded }}
149 onPress={() => props.onToggleRow(props.anchorKey, props.anchorKey)}
150 className="min-h-14 flex-row items-center gap-3 rounded-lg py-2 active:bg-subtle"
151 >
152 <View className="flex-row items-center">
153 {agents.slice(0, 3).map((agent, index) => (
154 <View key={agent.item.id} style={{ marginLeft: index === 0 ? 0 : -7 }}>
155 <SubagentAvatar item={agent.item} config={configOf(agent.item)} />
156 </View>
157 ))}
158 {agents.length > 3 ? (
159 <View className="-ml-2 h-7 w-7 items-center justify-center rounded-full border border-border bg-card">
160 <Text className="text-2xs text-foreground-muted">+{agents.length - 3}</Text>
161 </View>
162 ) : null}
163 </View>
164 <View className="min-w-0 flex-1 gap-0.5">
165 <Text numberOfLines={1} className="font-t3-medium text-sm text-foreground">
166 {label}
167 </Text>
168 <Text
169 numberOfLines={1}
170 className={cn(
171 "text-2xs text-foreground-muted",
172 agents.some((agent) => isActiveSubagentStatus(agent.status))
173 ? "text-adaptive-sky-600-400"
174 : agents.some((agent) => agent.status === "failed") &&
175 "text-adaptive-rose-600-400",
176 )}
177 >
178 {summary}
179 </Text>
180 </View>
181 <SubagentElapsed agents={agents} />
182 <SymbolView
183 name={expanded ? "chevron.up" : "chevron.down"}
184 size={11}
185 tintColor={props.iconSubtleColor}
186 />
187 </Pressable>
188 ) : null}
189 {!grouped || expanded ? (
190 <View className="mb-1 gap-px rounded-xl border border-border bg-card/30 p-1">
191 {agents.map((agent) => {
192 // A task in a linked environment opens there, if this app is connected to it.
193 const target = subagentThreadTarget(agent, props.environmentId, (id) =>
194 presentationById.has(id),
195 );
196 return (
197 <Pressable
198 key={agent.item.id}
199 accessible
200 accessibilityRole={target === null ? undefined : "link"}
201 accessibilityHint={
202 target !== null
203 ? "Opens this agent's thread"
204 : agent.remoteChild !== undefined
205 ? `Runs on ${agent.remoteChild.label}`
206 : "Provider-managed agent"
207 }
208 disabled={target === null}
209 onPress={() => {
210 if (target === null) return;
211 if (
212 openInspector(
213 { environmentId: props.environmentId, threadId: members[0]!.threadId },
214 {
215 type: "thread",
216 environmentId: target.environmentId,
217 threadId: target.threadId,
218 },
219 )
220 )
221 return;
222 navigation.dispatch(
223 StackActions.push("Thread", {
224 environmentId: String(target.environmentId),
225 threadId: String(target.threadId),
226 }),
227 );
228 }}
229 className="rounded-lg px-3 py-3 active:bg-subtle"
230 >
231 <SubagentRow
232 environmentId={props.environmentId}
233 subagent={agent}
234 canOpenRemote={target !== null}
235 elapsed={<SubagentElapsed agents={[agent]} />}
236 />
237 </Pressable>
238 );
239 })}
240 </View>
241 ) : null}
242 </WorkLogBlock>
243 );
244}