1import type {
2 OrchestrationV2TurnItemStatus,
3 OrchestrationV2ThreadShell,
4 OrchestrationProjectShell,
5 ServerProvider,
6} from "@t3tools/contracts";
7import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model";
8import { fileBasename } from "@t3tools/shared/path";
9import { isTerminalSubagentStatus } from "./subagentRuntime.ts";
10
11/** Summarizes one adjacent group, without changing its member identities or order. */
12export function subagentGroupSummary(
13 members: ReadonlyArray<{ readonly status: OrchestrationV2TurnItemStatus }>,
14) {
15 const active = members.some(
16 ({ status }) => status === "pending" || status === "running" || status === "waiting",
17 );
18 return {
19 label: `${active ? "Kicked off" : "Ran"} ${members.length} ${members.length === 1 ? "subagent" : "subagents"}`,
20 active,
21 failed: members.some(({ status }) => status === "failed"),
22 };
23}
24
25/**
26 * Counts a group's states in the order a reader scans them: what is still
27 * running first, then outcomes, using the agents panel's words.
28 */
29export function summarizeSubagentStatuses(
30 statuses: ReadonlyArray<OrchestrationV2TurnItemStatus>,
31): string {
32 const counts = { working: 0, done: 0, failed: 0, stopped: 0, idle: 0 };
33 for (const status of statuses) {
34 if (status === "pending" || status === "running" || status === "waiting") counts.working += 1;
35 else if (status === "completed") counts.done += 1;
36 else if (status === "failed") counts.failed += 1;
37 else if (status === "idle") counts.idle += 1;
38 else counts.stopped += 1;
39 }
40 return (Object.keys(counts) as Array<keyof typeof counts>)
41 .filter((key) => counts[key] > 0)
42 .map((key) => `${counts[key]} ${key}`)
43 .join(" · ");
44}
45
46/** Formats Codex task paths for display while leaving provider identity untouched. */
47export function formatSubagentDisplayTitle(title: string): string {
48 const displayTitle = title.replace(/^Subagent:\s*/i, "");
49 const path = /^\/root\/(?:[^/]+\/)*([^/]+)\/?$/u.exec(displayTitle);
50 if (path === null) return displayTitle;
51
52 const name = path[1]!.replace(/[_\s]+/gu, " ").trim();
53 return name.replace(/(^|\s)\S/gu, (letter) => letter.toUpperCase()) || displayTitle;
54}
55
56/** Match desktop's model resolution and show only changes from the parent's workspace. */
57export function resolveSubagentMetadata(input: {
58 readonly model: string | null;
59 readonly provider?: Pick<ServerProvider, "driver" | "models"> | null | undefined;
60 readonly parentThread?:
61 | Pick<OrchestrationV2ThreadShell, "projectId" | "worktreePath">
62 | null
63 | undefined;
64 readonly childThread?:
65 | Pick<OrchestrationV2ThreadShell, "branch" | "worktreePath">
66 | null
67 | undefined;
68 readonly parentProject?: Pick<OrchestrationProjectShell, "workspaceRoot"> | null | undefined;
69 readonly childProject?:
70 | Pick<OrchestrationProjectShell, "id" | "title" | "workspaceRoot">
71 | null
72 | undefined;
73}) {
74 const model = input.model?.trim();
75 const slug = input.provider
76 ? resolveSelectableModel(input.provider.driver, model, input.provider.models)
77 : model;
78 const catalogModel = input.provider?.models.find((candidate) => candidate.slug === slug);
79 const reportedLabel = catalogModel
80 ? catalogModel.shortName || catalogModel.name
81 : model
82 ? formatModelSlugName(model)
83 : "Not reported";
84 const qualifier = catalogModel?.subProvider?.trim();
85 const modelLabel = qualifier
86 ? reportedLabel
87 .replace(
88 new RegExp(
89 `^${qualifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s*[.:/-]\\s*|\\s+)`,
90 "iu",
91 ),
92 "",
93 )
94 .trim() || reportedLabel
95 : reportedLabel;
96 const parentWorkspace = input.parentThread?.worktreePath ?? input.parentProject?.workspaceRoot;
97 const childWorkspace = input.childThread?.worktreePath ?? input.childProject?.workspaceRoot;
98 const workspace = [
99 ...(input.parentThread &&
100 input.childProject &&
101 input.childProject.id !== input.parentThread.projectId
102 ? [{ label: "Project", value: input.childProject.title }]
103 : []),
104 ...(parentWorkspace && childWorkspace && parentWorkspace !== childWorkspace
105 ? [
106 {
107 label: input.childThread?.branch
108 ? "Branch"
109 : input.childThread?.worktreePath
110 ? "Worktree"
111 : "Workspace",
112 value: input.childThread?.branch ?? fileBasename(childWorkspace),
113 },
114 ]
115 : []),
116 ];
117 return { modelLabel, workspace };
118}
119
120/** Live work leads with progress; settled work leads with its result. */
121export function subagentDetailPreview(input: {
122 readonly status: OrchestrationV2TurnItemStatus;
123 readonly result?: string | null | undefined;
124 readonly progress?: string | null | undefined;
125}): string | null {
126 const result = input.result?.trim();
127 const progress = input.progress?.trim();
128 const detail =
129 (isTerminalSubagentStatus(input.status) ? result || progress : progress || result) || "";
130 const compact = detail.replace(/\s+/gu, " ");
131 return compact.length > 280 ? `${compact.slice(0, 280).trimEnd()}…` : compact || null;
132}