Skip to content
Source
Sign inSign up

threadAgentsPresentation.ts

254 lines7.9 KB

1import {
2 formatSubagentDisplayTitle,
3 resolveSubagentMetadata,
4 subagentDetailPreview,
5} from "@t3tools/client-runtime/state/subagent-display";
6import {
7 isActiveSubagentStatus,
8 type RuntimeSubagent,
9 type SubagentWorkflowGroup,
10} from "@t3tools/client-runtime/state/subagentRuntime";
11import { REASONING_EFFORT_OPTION_IDS } from "@t3tools/client-runtime/state/thread-execution";
12import type {
13 EnvironmentId,
14 OrchestrationV2Subagent,
15 ServerProvider,
16 ThreadId,
17} from "@t3tools/contracts";
18import { resolveSelectableModel } from "@t3tools/shared/model";
19​
20import { subagentCardDetail } from "./subagent-card-presentation";
21​
22const PROMPT_TITLE_LIMIT = 80;
23​
24export type SubagentRowTone = "working" | "completed" | "failed" | "stopped";
25​
26export interface SubagentRowPresentation {
27 readonly title: string;
28 readonly detail: string | null;
29 readonly statusLabel: string;
30 readonly tone: SubagentRowTone;
31 readonly live: boolean;
32 readonly canOpenThread: boolean;
33 /** The linked environment the task runs in, when it runs in one. */
34 readonly runsOn: string | null;
36​
37/**
38 * Where an agent's thread opens: here, or in the linked environment that runs
39 * it. Null when there is no thread, or this app is not connected to that
40 * environment.
41 */
42export function subagentThreadTarget(
43 subagent: Pick<OrchestrationV2Subagent, "childThreadId" | "remoteChild">,
44 environmentId: EnvironmentId,
45 isKnownEnvironment: (environmentId: EnvironmentId) => boolean,
46): { readonly environmentId: EnvironmentId; readonly threadId: ThreadId } | null {
47 if (subagent.childThreadId !== null) {
48 return { environmentId, threadId: subagent.childThreadId };
49 }
50 const remote = subagent.remoteChild;
51 return remote !== undefined && isKnownEnvironment(remote.environmentId)
52 ? { environmentId: remote.environmentId, threadId: remote.threadId }
53 : null;
55​
56function rowTitle(subagent: Pick<OrchestrationV2Subagent, "title" | "prompt">): string {
57 const title = subagent.title?.trim();
58 if (title) return formatSubagentDisplayTitle(title);
59 const prompt = subagent.prompt.trim();
60 if (prompt.length === 0) return "Subagent";
61 return prompt.length > PROMPT_TITLE_LIMIT
62 ? `${prompt.slice(0, PROMPT_TITLE_LIMIT - 3)}...`
63 : prompt;
65​
66function rowTone(status: OrchestrationV2Subagent["status"]): SubagentRowTone {
67 if (isActiveSubagentStatus(status)) return "working";
68 if (status === "completed") return "completed";
69 if (status === "failed") return "failed";
70 return "stopped";
72​
73function rowStatusLabel(status: OrchestrationV2Subagent["status"]): string {
74 switch (status) {
75 case "pending":
76 case "running":
77 return "Working";
78 case "waiting":
79 return "Waiting";
80 case "idle":
81 return "Idle";
82 case "completed":
83 return "Completed";
84 case "failed":
85 return "Failed";
86 case "cancelled":
87 return "Cancelled";
88 case "interrupted":
89 return "Interrupted";
90 }
92​
93export function resolveSubagentRowPresentation(
94 subagent: Pick<
95 OrchestrationV2Subagent,
96 "title" | "prompt" | "status" | "progress" | "result" | "childThreadId" | "remoteChild"
97 >,
98 canOpenRemote = false,
99): SubagentRowPresentation {
100 const live = isActiveSubagentStatus(subagent.status);
101 return {
102 title: rowTitle(subagent),
103 detail: subagentDetailPreview(subagent),
104 statusLabel: rowStatusLabel(subagent.status),
105 tone: rowTone(subagent.status),
106 live,
107 canOpenThread:
108 subagent.childThreadId !== null || (subagent.remoteChild !== undefined && canOpenRemote),
109 runsOn: subagent.remoteChild?.label ?? null,
110 };
112​
113export type WorkflowRailTone = SubagentRowTone | "queued";
114​
115export interface WorkflowRailItem {
116 readonly key: string;
117 readonly title: string;
118 readonly members: ReadonlyArray<RuntimeSubagent>;
119 readonly completedCount: number;
120 readonly tone: WorkflowRailTone;
122​
123function railTone(members: ReadonlyArray<RuntimeSubagent>): WorkflowRailTone {
124 if (members.every((member) => member.status === "pending")) return "queued";
125 if (members.some((member) => isActiveSubagentStatus(member.status) || member.status === "idle")) {
126 return "working";
127 }
128 if (members.some((member) => member.status === "failed")) return "failed";
129 return members.every((member) => member.status === "completed") ? "completed" : "stopped";
131​
132export function workflowRail(group: SubagentWorkflowGroup): ReadonlyArray<WorkflowRailItem> {
133 const items = group.phases.map((phase) => ({
134 key: String(phase.index),
135 title: phase.title,
136 members: phase.members,
137 }));
138 if (group.unphasedMembers.length > 0) {
139 items.push({
140 key: "unphased",
141 title: items.length === 0 ? "Agents" : "Other",
142 members: group.unphasedMembers,
143 });
144 }
145 return items.map((item) => ({
146 ...item,
147 completedCount: item.members.filter((member) => member.status === "completed").length,
148 tone: railTone(item.members),
149 }));
151​
152export function nextWorkflowSelection(
153 rail: ReadonlyArray<WorkflowRailItem>,
154 selected: string | null,
155 tapped?: string,
156): string | null {
157 const open = rail.some((item) => item.key === selected) ? selected : null;
158 if (tapped !== undefined) return tapped === open ? null : tapped;
159 if (open !== null) return null;
160 const filled = rail.filter((item) => item.members.length > 0);
161 return (
162 (
163 filled.find((item) => item.tone === "working") ??
164 filled.find((item) => item.tone === "failed") ??
165 filled.at(-1)
166 )?.key ?? null
167 );
169​
170function capitalize(value: string): string {
171 return value.charAt(0).toUpperCase() + value.slice(1);
173​
174export function workflowMemberMetadata(
175 member: Pick<RuntimeSubagent, "model" | "effort" | "speed">,
176 provider: Pick<ServerProvider, "driver" | "models"> | undefined,
177) {
178 const model = member.model?.trim()
179 ? resolveSubagentMetadata({ model: member.model, provider }).modelLabel
180 : null;
181 const slug = provider
182 ? resolveSelectableModel(provider.driver, member.model, provider.models)
183 : null;
184 const effortChoice = provider?.models
185 .find((candidate) => candidate.slug === slug)
186 ?.capabilities?.optionDescriptors?.flatMap((descriptor) =>
187 descriptor.type === "select" && REASONING_EFFORT_OPTION_IDS.includes(descriptor.id)
188 ? descriptor.options
189 : [],
190 )
191 .find((option) => option.id === member.effort);
192 return {
193 model,
194 effort: member.effort ? (effortChoice?.label ?? capitalize(member.effort)) : null,
195 speed: member.speed ? capitalize(member.speed) : null,
196 fast: member.speed === "fast",
197 };
199​
200export function workflowMemberDetail(
201 member: Pick<RuntimeSubagent, "status" | "progress" | "result" | "error">,
202) {
203 return {
204 detail: subagentCardDetail(subagentDetailPreview(member)),
205 error: member.status === "failed" ? subagentCardDetail(member.error) : null,
206 };
208​
209export function resolveSubagentStopTarget(
210 subagent: Pick<
211 OrchestrationV2Subagent,
212 | "id"
213 | "threadId"
214 | "childThreadId"
215 | "status"
216 | "origin"
217 | "driver"
218 | "runId"
219 | "nativeTaskRef"
220 | "workflow"
221 >,
222) {
223 if (!isActiveSubagentStatus(subagent.status)) return null;
224 if (subagent.workflow !== undefined) {
225 return subagent.status === "running" &&
226 subagent.origin === "provider_native" &&
227 subagent.driver === "claudeAgent" &&
228 Boolean(subagent.nativeTaskRef?.nativeId)
229 ? {
230 kind: "workflow" as const,
231 input: { threadId: subagent.threadId, subagentId: subagent.id },
232 }
233 : null;
234 }
235 if (subagent.origin === "app_owned" && subagent.childThreadId !== null) {
236 return {
237 kind: "subagent" as const,
238 input: { threadId: subagent.threadId, subagentId: subagent.id },
239 };
240 }
241 if (
242 subagent.origin === "provider_native" &&
243 subagent.driver === "claudeAgent" &&
244 subagent.runId !== null &&
245 subagent.nativeTaskRef?.strength === "strong" &&
246 subagent.nativeTaskRef.nativeId !== null
247 ) {
248 return {
249 kind: "subagent" as const,
250 input: { threadId: subagent.threadId, subagentId: subagent.id },
251 };
252 }
253 return null;