Open History to see the last commit
1import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
2import {
3 normalizeProviderAccentColor,
4 resolveProviderInstanceDisplayName,
5 shouldShowInstanceBadge,
6} from "@t3tools/client-runtime/state/provider-instance-display";
7import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display";
8import { formatSubagentEffort } from "@t3tools/client-runtime/state/thread-execution";
9import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts";
10import type { ReactNode } from "react";
11import { View } from "react-native";
12
13import { AppText as Text } from "../../components/ui/AppText";
14import { SymbolView } from "../../components/ui/AppSymbol";
15import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIcon";
16import { cn } from "../../lib/cn";
17import { useEnvironmentServerConfig, useProject, useThreadShell } from "../../state/entities";
18import { subagentCardDetail } from "./subagent-card-presentation";
19import { SUBAGENT_TONE_TEXT_CLASS, SubagentStatusDot } from "./SubagentStatusDot";
20import { resolveSubagentRowPresentation } from "./threadAgentsPresentation";
21
22type SubagentRowSubagent = Pick<
23 OrchestrationV2Subagent,
24 | "threadId"
25 | "childThreadId"
26 | "model"
27 | "modelSelection"
28 | "driver"
29 | "providerInstanceId"
30 | "title"
31 | "prompt"
32 | "status"
33 | "progress"
34 | "result"
35 | "remoteChild"
36>;
37
38export function SubagentRow(props: {
39 readonly environmentId: EnvironmentId;
40 readonly subagent: SubagentRowSubagent;
41 readonly elapsed: ReactNode;
42 /** The task runs in a linked environment this app is connected to. */
43 readonly canOpenRemote?: boolean;
44}) {
45 const presentation = resolveSubagentRowPresentation(props.subagent, props.canOpenRemote);
46 const detail = subagentCardDetail(presentation.detail);
47 return (
48 <View className="flex-row gap-3">
49 <View className="h-5 justify-center">
50 <SubagentStatusDot tone={presentation.tone} placement="sheet" />
51 </View>
52 <View className="min-w-0 flex-1 gap-1">
53 <View className="min-h-5 flex-row items-center gap-2">
54 <View className="min-w-0 flex-1 flex-row items-baseline gap-1.5">
55 <Text
56 numberOfLines={1}
57 className="min-w-0 shrink font-t3-medium text-sm text-foreground"
58 >
59 {presentation.title}
60 </Text>
61 <Text
62 accessibilityElementsHidden
63 importantForAccessibility="no"
64 className="shrink-0 text-xs text-foreground-muted"
65 >
66 ·
67 </Text>
68 <Text
69 className={cn(
70 "shrink-0 text-xs font-t3-medium",
71 SUBAGENT_TONE_TEXT_CLASS[presentation.tone],
72 )}
73 >
74 {presentation.statusLabel}
75 </Text>
76 {presentation.runsOn !== null ? (
77 <Text numberOfLines={1} className="shrink text-xs text-foreground-muted">
78 on {presentation.runsOn}
79 </Text>
80 ) : null}
81 </View>
82 {props.elapsed}
83 {presentation.canOpenThread ? (
84 <SymbolView name="chevron.right" size={12} tintColorClassName="accent-icon-subtle" />
85 ) : null}
86 </View>
87 <SubagentMetadata environmentId={props.environmentId} subagent={props.subagent} />
88 {detail ? (
89 <Text
90 numberOfLines={3}
91 className={cn(
92 "text-xs text-foreground-muted",
93 presentation.tone === "failed" && SUBAGENT_TONE_TEXT_CLASS.failed,
94 )}
95 >
96 {detail}
97 </Text>
98 ) : null}
99 </View>
100 </View>
101 );
102}
103
104function SubagentMetadata(props: {
105 readonly environmentId: EnvironmentId;
106 readonly subagent: SubagentRowSubagent;
107}) {
108 const { environmentId, subagent } = props;
109 // A task in a linked environment runs a provider instance of that environment.
110 const config = useEnvironmentServerConfig(subagent.remoteChild?.environmentId ?? environmentId);
111 const providers = (config?.providers ?? []).map((candidate) => {
112 const rawConfig = config?.settings.providerInstances[candidate.instanceId]?.config;
113 const instanceConfig =
114 rawConfig !== null && typeof rawConfig === "object"
115 ? (rawConfig as Readonly<Record<string, unknown>>)
116 : null;
117 const agentId = instanceConfig?.source === "local" ? undefined : instanceConfig?.agentId;
118 return {
119 ...candidate,
120 driverKind: candidate.driver,
121 accentColor: normalizeProviderAccentColor(candidate.accentColor),
122 acpRegistryAgentId: typeof agentId === "string" ? agentId.trim() || undefined : undefined,
123 };
124 });
125 const provider = providers.find(
126 (candidate) => candidate.instanceId === subagent.providerInstanceId,
127 );
128 const showBadge = provider !== undefined && shouldShowInstanceBadge(provider, providers);
129 const parent = useThreadShell(scopeThreadRef(environmentId, subagent.threadId))?.source;
130 const child = useThreadShell(
131 subagent.childThreadId === null ? null : scopeThreadRef(environmentId, subagent.childThreadId),
132 )?.source;
133 const parentProject = useProject(
134 parent ? scopeProjectRef(environmentId, parent.projectId) : null,
135 );
136 const childProject = useProject(child ? scopeProjectRef(environmentId, child.projectId) : null);
137 const selection = subagent.modelSelection;
138 const { modelLabel, modelSlug, workspace } = resolveSubagentMetadata({
139 model: subagent.model,
140 modelSelection: selection,
141 provider,
142 parentThread: parent,
143 childThread: child,
144 parentProject,
145 childProject,
146 });
147 const effort = formatSubagentEffort({ modelSlug, modelSelection: selection, provider });
148 return (
149 <View className="min-w-0 flex-row items-center gap-1.5">
150 {showBadge ? (
151 <ProviderInstanceIcon
152 provider={provider.driver}
153 iconUrl={provider.iconUrl}
154 size={12}
155 displayName={resolveProviderInstanceDisplayName(provider)}
156 accentColor={provider.accentColor}
157 showBadge
158 />
159 ) : (
160 <ProviderIcon
161 provider={provider?.driver ?? subagent.driver}
162 iconUrl={provider?.iconUrl}
163 size={12}
164 />
165 )}
166 <Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
167 {provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""}
168 {modelLabel}
169 {effort ? ` · ${effort}` : ""}
170 </Text>
171 {workspace.map(({ label, value }) => (
172 // Preserve the label against flattening without making it a separate accessibility stop.
173 <View
174 key={label}
175 collapsable={false}
176 accessibilityLabel={`${label}: ${value}`}
177 className="min-w-0 shrink flex-row items-center gap-1.5"
178 >
179 <Text className="text-xs text-foreground-muted">·</Text>
180 <SymbolView
181 name={label === "Branch" ? "arrow.triangle.branch" : "folder"}
182 size={11}
183 tintColorClassName="accent-icon-muted"
184 />
185 <Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
186 {value}
187 </Text>
188 </View>
189 ))}
190 </View>
191 );
192}