192 lines · 6.7 KB · TypeScript
RawDownload
import { isToolLifecycleItemType, type ToolLifecycleItemType } from "@t3tools/contracts"; export function isWorktreeSetupActivity(kind: string): boolean {  return kind === "setup-script.requested" || kind === "setup-script.started";} export interface WorkLogPresentationEntry {  readonly label: string;  readonly toolTitle?: string;  readonly tone: "thinking" | "tool" | "info" | "error";  readonly command?: string;  readonly changedFiles?: ReadonlyArray<string>;  readonly itemType?: ToolLifecycleItemType;  readonly requestKind?: string;  readonly turnId?: string | null;  readonly toolCallId?: string;  readonly toolLifecycleStatus?: string;  readonly sourceActivityKind?: string;  readonly taskId?: string;} export type ToolGroupAction =  | "read"  | "edit"  | "command"  | "code-search"  | "search"  | "other"  | "update"; export type ToolGroupSummaryKind =  | ToolGroupAction  | "dynamic-tool"  | "agent-tool"  | "tone-tool"  | "mixed"; export function normalizeCompactToolLabel(value: string): string {  return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();} function workLogEntryIsToolLike(entry: WorkLogPresentationEntry): boolean {  if (entry.tone === "tool" || entry.tone === "thinking" || entry.tone === "error") return true;  if (entry.command !== undefined && entry.command.trim().length > 0) return true;  if (entry.requestKind !== undefined) return true;  return entry.itemType !== undefined && isToolLifecycleItemType(entry.itemType);} export function workLogEntryIsLocalCodeSearch(entry: WorkLogPresentationEntry): boolean {  return (    entry.itemType === "web_search" &&    /\bgrep\b/i.test(normalizeCompactToolLabel(entry.toolTitle ?? entry.label))  );} export function toolGroupAction(entry: WorkLogPresentationEntry): ToolGroupAction {  if (    entry.requestKind === "file-read" ||    entry.itemType === "image_view" ||    (entry.itemType === "dynamic_tool_call" && entry.toolTitle === "Read File")  ) {    return "read";  }  if (    entry.requestKind === "file-change" ||    entry.itemType === "file_change" ||    (entry.changedFiles?.length ?? 0) > 0  ) {    return "edit";  }  if (entry.requestKind === "command" || entry.itemType === "command_execution" || entry.command) {    return "command";  }  if (workLogEntryIsLocalCodeSearch(entry)) return "code-search";  if (entry.itemType === "web_search") return "search";  return workLogEntryIsToolLike(entry) ? "other" : "update";} function toolGroupActionCount(  action: ToolGroupAction,  entries: ReadonlyArray<WorkLogPresentationEntry>,): number {  if (action !== "edit") return entries.length;   const changedFiles = new Set<string>();  let editsWithoutFileDetails = 0;  for (const entry of entries) {    if (!entry.changedFiles || entry.changedFiles.length === 0) {      editsWithoutFileDetails += 1;      continue;    }    for (const file of entry.changedFiles) changedFiles.add(file);  }  return changedFiles.size + editsWithoutFileDetails;} function toolGroupActionLabel(action: ToolGroupAction, count: number): string {  switch (action) {    case "read":      return `Read ${count} ${count === 1 ? "file" : "files"}`;    case "edit":      return `Changed ${count} ${count === 1 ? "file" : "files"}`;    case "command":      return `Ran ${count} ${count === 1 ? "command" : "commands"}`;    case "search":      return `Searched the web ${count} ${count === 1 ? "time" : "times"}`;    case "code-search":      return `Searched code ${count} ${count === 1 ? "time" : "times"}`;    case "other":      return `Used ${count} ${count === 1 ? "tool" : "tools"}`;    case "update":      return `Received ${count} ${count === 1 ? "update" : "updates"}`;  }} export function summarizeToolGroup(entries: ReadonlyArray<WorkLogPresentationEntry>): string {  const summaryEntries = omitSupersededLifecycleMarkers(entries, (entry) => entry);  const groupedEntries = new Map<ToolGroupAction, WorkLogPresentationEntry[]>();  for (const entry of summaryEntries) {    const action = toolGroupAction(entry);    const group = groupedEntries.get(action);    if (group) group.push(entry);    else groupedEntries.set(action, [entry]);  }  const labels = [...groupedEntries].map(([action, actionEntries]) =>    toolGroupActionLabel(action, toolGroupActionCount(action, actionEntries)),  );  const sentenceLabels = labels.map((label, index) =>    index === 0 ? label : label.charAt(0).toLowerCase() + label.slice(1),  );  if (sentenceLabels.length < 2) return sentenceLabels[0] ?? "";  if (sentenceLabels.length === 2) return sentenceLabels.join(" and ");  return `${sentenceLabels.slice(0, -1).join(", ")}, and ${sentenceLabels.at(-1)}`;} export function omitSupersededLifecycleMarkers<T>(  entries: readonly T[],  workEntryFor: (entry: T) => WorkLogPresentationEntry,): T[] {  const laterTerminalIdentities = new Set<string>();  const reversedEntries: T[] = [];   for (let index = entries.length - 1; index >= 0; index -= 1) {    const entry = entries[index]!;    const workEntry = workEntryFor(entry);    const normalizedLabel = normalizeCompactToolLabel(workEntry.toolTitle ?? workEntry.label);    const identity = [      workEntry.turnId ?? "no-turn",      workEntry.itemType ?? "",      normalizedLabel,    ].join("\u001f");    const activityKind = workEntry.sourceActivityKind;    const isStatuslessIdlessMarker =      workEntry.toolCallId === undefined &&      workEntry.toolLifecycleStatus === undefined &&      (activityKind === "tool.started" || activityKind === "tool.updated");    if (isStatuslessIdlessMarker && laterTerminalIdentities.has(identity)) continue;     reversedEntries.push(entry);    if (      activityKind === "tool.completed" ||      (workEntry.toolLifecycleStatus !== undefined &&        workEntry.toolLifecycleStatus !== "inProgress")    ) {      laterTerminalIdentities.add(identity);    }  }   return reversedEntries.toReversed();} export function toolGroupSummaryKind(  entries: ReadonlyArray<WorkLogPresentationEntry>,): ToolGroupSummaryKind {  const actions = new Set(entries.map(toolGroupAction));  if (actions.size !== 1) return "mixed";   const action = actions.values().next().value!;  if (action !== "other") return action;   const fallbackKinds = new Set(    entries.map((entry): ToolGroupSummaryKind => {      if (entry.itemType === "mcp_tool_call") return "other";      if (entry.itemType === "dynamic_tool_call") return "dynamic-tool";      if (entry.itemType === "collab_agent_tool_call" || entry.taskId) return "agent-tool";      if (entry.tone === "thinking") return "agent-tool";      if (entry.tone === "tool") return "tone-tool";      return "other";    }),  );  return fallbackKinds.size === 1 ? fallbackKinds.values().next().value! : "mixed";}