| 1 | import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 2 | import { resolveProviderInstanceDisplayName } from "@t3tools/client-runtime/state/provider-instance-display"; |
| 3 | import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display"; |
| 4 | import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts"; |
| 5 | import type { ReactNode } from "react"; |
| 6 | import { View } from "react-native"; |
| 7 | |
| 8 | import { AppText as Text } from "../../components/AppText"; |
| 9 | import { SymbolView } from "../../components/AppSymbol"; |
| 10 | import { ProviderIcon } from "../../components/ProviderIcon"; |
| 11 | import { cn } from "../../lib/cn"; |
| 12 | import { useEnvironmentServerConfig, useProject, useThreadShell } from "../../state/entities"; |
| 13 | import { subagentCardDetail } from "./subagent-card-presentation"; |
| 14 | import { SUBAGENT_TONE_TEXT_CLASS, SubagentStatusDot } from "./SubagentStatusDot"; |
| 15 | import { resolveSubagentRowPresentation } from "./threadAgentsPresentation"; |
| 16 | |
| 17 | type 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 | */ |
| 36 | export 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. */ |
| 96 | function 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 | } |