Skip to content
Source
Sign inSign up

subagentDisplay.ts

135 lines5.1 KB

1import type {
2 ModelSelection,
3 OrchestrationV2TurnItemStatus,
4 OrchestrationV2ThreadShell,
5 OrchestrationProjectShell,
6 ServerProvider,
7} from "@t3tools/contracts";
8import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model";
9import { fileBasename } from "@t3tools/shared/path";
10import { resolveModelShortName } from "../providerClients.ts";
11import { isTerminalSubagentStatus } from "./subagentRuntime.ts";
12​
13export function summarizeSubagentStatuses(
14 statuses: ReadonlyArray<OrchestrationV2TurnItemStatus>,
15): string {
16 const counts = { working: 0, done: 0, failed: 0, stopped: 0, idle: 0 };
17 for (const status of statuses) {
18 if (status === "pending" || status === "running" || status === "waiting") counts.working += 1;
19 else if (status === "completed") counts.done += 1;
20 else if (status === "failed") counts.failed += 1;
21 else if (status === "idle") counts.idle += 1;
22 else counts.stopped += 1;
23 }
24 return (Object.keys(counts) as Array<keyof typeof counts>)
25 .filter((key) => counts[key] > 0)
26 .map((key) => `${counts[key]} ${key}`)
27 .join(" · ");
29​
30export function formatSubagentDisplayTitle(title: string): string {
31 const displayTitle = title.replace(/^Subagent:\s*/i, "");
32 const path = /^\/root\/(?:[^/]+\/)*([^/]+)\/?$/u.exec(displayTitle);
33 if (path === null) return displayTitle;
34​
35 const name = path[1]!.replace(/[_\s]+/gu, " ").trim();
36 return name.replace(/(^|\s)\S/gu, (letter) => letter.toUpperCase()) || displayTitle;
38​
39export function resolveSubagentMetadata(input: {
40 readonly model: string | null;
41 readonly modelSelection?: Pick<ModelSelection, "model"> | undefined;
42 readonly provider?: Pick<ServerProvider, "driver" | "models"> | null | undefined;
43 readonly parentThread?:
44 | Pick<OrchestrationV2ThreadShell, "projectId" | "worktreePath">
45 | null
46 | undefined;
47 readonly childThread?:
48 | Pick<OrchestrationV2ThreadShell, "branch" | "worktreePath">
49 | null
50 | undefined;
51 readonly parentProject?: Pick<OrchestrationProjectShell, "workspaceRoot"> | null | undefined;
52 readonly childProject?:
53 | Pick<OrchestrationProjectShell, "id" | "title" | "workspaceRoot">
54 | null
55 | undefined;
56}) {
57 const model = input.model?.trim() || input.modelSelection?.model.trim();
58 const slug = input.provider
59 ? resolveSelectableModel(input.provider.driver, model, input.provider.models)
60 : model;
61 const catalogModel = input.provider?.models.find((candidate) => candidate.slug === slug);
62 const reportedLabel = catalogModel
63 ? (resolveModelShortName(catalogModel, input.provider?.driver) ?? catalogModel.name)
64 : model
65 ? formatModelSlugName(model)
66 : "Not reported";
67 const qualifier = catalogModel?.subProvider?.trim();
68 const modelLabel = qualifier
69 ? reportedLabel
70 .replace(
71 new RegExp(
72 `^${qualifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s*[.:/-]\\s*|\\s+)`,
73 "iu",
74 ),
75 "",
76 )
77 .trim() || reportedLabel
78 : reportedLabel;
79 const parentWorkspace = input.parentThread?.worktreePath ?? input.parentProject?.workspaceRoot;
80 const childWorkspace = input.childThread?.worktreePath ?? input.childProject?.workspaceRoot;
81 const workspace = [
82 ...(input.parentThread &&
83 input.childProject &&
84 input.childProject.id !== input.parentThread.projectId
85 ? [{ label: "Project", value: input.childProject.title }]
86 : []),
87 ...(parentWorkspace && childWorkspace && parentWorkspace !== childWorkspace
88 ? [
89 {
90 label: input.childThread?.branch
91 ? "Branch"
92 : input.childThread?.worktreePath
93 ? "Worktree"
94 : "Workspace",
95 value: input.childThread?.branch ?? fileBasename(childWorkspace),
96 },
97 ]
98 : []),
99 ];
100 return { modelLabel, modelSlug: slug ?? model ?? null, workspace };
102​
103export function subagentDetailPreview(input: {
104 readonly status: OrchestrationV2TurnItemStatus;
105 readonly result?: string | null | undefined;
106 readonly progress?: string | null | undefined;
107}): string | null {
108 const terminal = isTerminalSubagentStatus(input.status);
109 const preferred = terminal ? input.result : input.progress;
110 const fallback = terminal ? input.progress : input.result;
111 const detail = preferred?.trimStart() || fallback?.trimStart() || "";
112 if (detail.length <= 280) return detail.trimEnd().replace(/\s+/gu, " ") || null;
113 const whitespace = /\s+/uy;
114 let compact = "";
115 for (let index = 0; index < detail.length; index += 1) {
116 whitespace.lastIndex = index;
117 const gap = whitespace.exec(detail);
118 if (gap) {
119 index += gap[0].length;
120 if (index === detail.length) break;
121 if (compact.length > 0) compact += " ";
122 }
123 compact += detail[index];
124 if (compact.length > 280) {
125 let end = 280;
126 const previous = compact.charCodeAt(end - 1);
127 const next = compact.charCodeAt(end);
128 if (previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
129 end -= 1;
130 }
131 return `${compact.slice(0, end).trimEnd()}…`;
132 }
133 }
134 return compact || null;