1import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
2import { resolveProviderInstanceDisplayName } from "@t3tools/client-runtime/state/provider-instance-display";
3import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display";
4import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts";
5import type { ReactNode } from "react";
6import { View } from "react-native";
7
8import { AppText as Text } from "../../components/AppText";
9import { SymbolView } from "../../components/AppSymbol";
10import { ProviderIcon } from "../../components/ProviderIcon";
11import { cn } from "../../lib/cn";
12import { useEnvironmentServerConfig, useProject, useThreadShell } from "../../state/entities";
13import { subagentCardDetail } from "./subagent-card-presentation";
14import { SUBAGENT_TONE_TEXT_CLASS, SubagentStatusDot } from "./SubagentStatusDot";
15import { resolveSubagentRowPresentation } from "./threadAgentsPresentation";
16
17type SubagentRowSubagent = Pick<
18 OrchestrationV2Subagent,
19 | "threadId"
20 | "childThreadId"
21 | "model"
22 | "driver"
23 | "providerInstanceId"
24 | "title"
25 | "prompt"
26 | "status"
27 | "progress"
28 | "result"
29>;
30
31/**
32 * One agent as shown in the Agents sheet and transcript cards: status, model,
33 * workspace changes, and recent progress or result. Callers own the press
34 * target and pass their own elapsed timer so ticking stays isolated.
35 */
36export function SubagentRow(props: {
37 readonly environmentId: EnvironmentId;
38 readonly subagent: SubagentRowSubagent;
39 readonly elapsed: ReactNode;
40}) {
41 const presentation = resolveSubagentRowPresentation(props.subagent);
42 const detail = subagentCardDetail(presentation.detail);
43 return (
44 <View className="flex-row gap-3">
45 <View className="h-5 justify-center">
46 <SubagentStatusDot tone={presentation.tone} placement="sheet" />
47 </View>
48 <View className="min-w-0 flex-1 gap-1">
49 <View className="min-h-5 flex-row items-center gap-2">
50 <View className="min-w-0 flex-1 flex-row items-baseline gap-1.5">
51 <Text
52 numberOfLines={1}
53 className="min-w-0 shrink font-t3-medium text-sm text-foreground"
54 >
55 {presentation.title}
56 </Text>
57 <Text
58 accessibilityElementsHidden
59 importantForAccessibility="no"
60 className="shrink-0 text-xs text-foreground-muted"
61 >
62 ·
63 </Text>
64 <Text
65 className={cn(
66 "shrink-0 text-xs font-t3-medium",
67 SUBAGENT_TONE_TEXT_CLASS[presentation.tone],
68 )}
69 >
70 {presentation.statusLabel}
71 </Text>
72 </View>
73 {props.elapsed}
74 {presentation.canOpenThread ? (
75 <SymbolView name="chevron.right" size={12} tintColorClassName="accent-icon-subtle" />
76 ) : null}
77 </View>
78 <SubagentMetadata environmentId={props.environmentId} subagent={props.subagent} />
79 {detail ? (
80 <Text
81 numberOfLines={3}
82 className={cn(
83 "text-xs text-foreground-muted",
84 presentation.tone === "failed" && SUBAGENT_TONE_TEXT_CLASS.failed,
85 )}
86 >
87 {detail}
88 </Text>
89 ) : null}
90 </View>
91 </View>
92 );
93}
94
95/** Uses shell data already held by the client, without loading child transcripts. */
96function SubagentMetadata(props: {
97 readonly environmentId: EnvironmentId;
98 readonly subagent: SubagentRowSubagent;
99}) {
100 const { environmentId, subagent } = props;
101 const config = useEnvironmentServerConfig(environmentId);
102 const provider = config?.providers.find(
103 (candidate) => candidate.instanceId === subagent.providerInstanceId,
104 );
105 const parent = useThreadShell(scopeThreadRef(environmentId, subagent.threadId))?.source;
106 const child = useThreadShell(
107 subagent.childThreadId === null ? null : scopeThreadRef(environmentId, subagent.childThreadId),
108 )?.source;
109 const parentProject = useProject(
110 parent ? scopeProjectRef(environmentId, parent.projectId) : null,
111 );
112 const childProject = useProject(child ? scopeProjectRef(environmentId, child.projectId) : null);
113 const { modelLabel, workspace } = resolveSubagentMetadata({
114 model: subagent.model,
115 provider,
116 parentThread: parent,
117 childThread: child,
118 parentProject,
119 childProject,
120 });
121 return (
122 <View className="min-w-0 flex-row items-center gap-1.5">
123 <ProviderIcon
124 provider={provider?.driver ?? subagent.driver}
125 iconUrl={provider?.iconUrl}
126 size={12}
127 />
128 <Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
129 {provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""}
130 {modelLabel}
131 </Text>
132 {workspace.map(({ label, value }) => (
133 // The row reads this label in place of the icon. collapsable keeps the
134 // view (and label) from being flattened away without making it a
135 // separate accessibility stop.
136 <View
137 key={label}
138 collapsable={false}
139 accessibilityLabel={`${label}: ${value}`}
140 className="min-w-0 shrink flex-row items-center gap-1.5"
141 >
142 <Text className="text-xs text-foreground-muted">·</Text>
143 <SymbolView
144 name={label === "Branch" ? "arrow.triangle.branch" : "folder"}
145 size={11}
146 tintColorClassName="accent-icon-muted"
147 />
148 <Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
149 {value}
150 </Text>
151 </View>
152 ))}
153 </View>
154 );
155}