| 1 | import type { |
| 2 | OrchestrationV2TurnItemStatus, |
| 3 | OrchestrationV2ThreadShell, |
| 4 | OrchestrationProjectShell, |
| 5 | ServerProvider, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model"; |
| 8 | import { fileBasename } from "@t3tools/shared/path"; |
| 9 | import { isTerminalSubagentStatus } from "./subagentRuntime.ts"; |
| 10 | |
| 11 | /** Summarizes one adjacent group, without changing its member identities or order. */ |
| 12 | export 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 | */ |
| 29 | export 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. */ |
| 47 | export 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. */ |
| 57 | export 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. */ |
| 121 | export 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 | } |