195 lines · 6.0 KB · TypeScript
RawDownload
import {  ApprovalRequestId,  type OrchestrationThreadActivity,  type ProviderKind,  type TurnId,} from "@t3tools/contracts"; import type { ChatMessage, SessionPhase, ThreadSession, TurnDiffSummary } from "./types"; export const PROVIDER_OPTIONS: Array<{  value: ProviderKind;  label: string;  available: boolean;}> = [  { value: "codex", label: "Codex", available: true },  { value: "claudeCode", label: "Claude Code (soon)", available: false },]; export interface WorkLogEntry {  id: string;  createdAt: string;  label: string;  detail?: string;  tone: "thinking" | "tool" | "info" | "error";} export interface PendingApproval {  requestId: ApprovalRequestId;  requestKind: "command" | "file-change";  createdAt: string;  detail?: string;} export type TimelineEntry =  | {      id: string;      kind: "message";      createdAt: string;      message: ChatMessage;    }  | {      id: string;      kind: "work";      createdAt: string;      entry: WorkLogEntry;    }; export function formatTimestamp(isoDate: string): string {  return new Intl.DateTimeFormat(undefined, {    hour: "numeric",    minute: "2-digit",    second: "2-digit",  }).format(new Date(isoDate));} export function formatDuration(durationMs: number): string {  if (!Number.isFinite(durationMs) || durationMs < 0) return "0ms";  if (durationMs < 1_000) return `${Math.max(1, Math.round(durationMs))}ms`;  if (durationMs < 10_000) return `${(durationMs / 1_000).toFixed(1)}s`;  if (durationMs < 60_000) return `${Math.round(durationMs / 1_000)}s`;  const minutes = Math.floor(durationMs / 60_000);  const seconds = Math.round((durationMs % 60_000) / 1_000);  if (seconds === 0) return `${minutes}m`;  if (seconds === 60) return `${minutes + 1}m`;  return `${minutes}m ${seconds}s`;} export function formatElapsed(startIso: string, endIso: string | undefined): string | null {  if (!endIso) return null;  const startedAt = Date.parse(startIso);  const endedAt = Date.parse(endIso);  if (Number.isNaN(startedAt) || Number.isNaN(endedAt) || endedAt < startedAt) {    return null;  }  return formatDuration(endedAt - startedAt);} export function derivePendingApprovals(  activities: ReadonlyArray<OrchestrationThreadActivity>,): PendingApproval[] {  const openByRequestId = new Map<ApprovalRequestId, PendingApproval>();  const ordered = [...activities].toSorted((left, right) =>    left.createdAt.localeCompare(right.createdAt),  );   for (const activity of ordered) {    const payload =      activity.payload && typeof activity.payload === "object"        ? (activity.payload as Record<string, unknown>)        : null;    const requestId =      payload && typeof payload.requestId === "string"        ? ApprovalRequestId.makeUnsafe(payload.requestId)        : null;    const requestKind =      payload && (payload.requestKind === "command" || payload.requestKind === "file-change")        ? payload.requestKind        : null;    const detail = payload && typeof payload.detail === "string" ? payload.detail : undefined;     if (activity.kind === "approval.requested" && requestId && requestKind) {      openByRequestId.set(requestId, {        requestId,        requestKind,        createdAt: activity.createdAt,        ...(detail ? { detail } : {}),      });      continue;    }     if (activity.kind === "approval.resolved" && requestId) {      openByRequestId.delete(requestId);    }  }   return [...openByRequestId.values()].toSorted((left, right) =>    left.createdAt.localeCompare(right.createdAt),  );} export function deriveWorkLogEntries(  activities: ReadonlyArray<OrchestrationThreadActivity>,  latestTurnId: TurnId | undefined,): WorkLogEntry[] {  const ordered = [...activities].toSorted((left, right) =>    left.createdAt.localeCompare(right.createdAt),  );  return ordered    .filter((activity) => (latestTurnId ? activity.turnId === latestTurnId : true))    .filter((activity) => activity.kind !== "tool.started")    .filter((activity) => activity.summary !== "Checkpoint captured")    .map((activity) => {      const payload =        activity.payload && typeof activity.payload === "object"          ? (activity.payload as Record<string, unknown>)          : null;      const entry: WorkLogEntry = {        id: activity.id,        createdAt: activity.createdAt,        label: activity.summary,        tone: activity.tone === "approval" ? "info" : activity.tone,      };      if (payload && typeof payload.detail === "string" && payload.detail.length > 0) {        entry.detail = payload.detail;      }      return entry;    });} export function hasToolActivityForTurn(  activities: ReadonlyArray<OrchestrationThreadActivity>,  turnId: TurnId | null | undefined,): boolean {  if (!turnId) return false;  return activities.some((activity) => activity.turnId === turnId && activity.tone === "tool");} export function deriveTimelineEntries(  messages: ChatMessage[],  workEntries: WorkLogEntry[],): TimelineEntry[] {  const messageRows: TimelineEntry[] = messages.map((message) => ({    id: message.id,    kind: "message",    createdAt: message.createdAt,    message,  }));  const workRows: TimelineEntry[] = workEntries.map((entry) => ({    id: entry.id,    kind: "work",    createdAt: entry.createdAt,    entry,  }));  return [...messageRows, ...workRows].toSorted((a, b) => a.createdAt.localeCompare(b.createdAt));} export function inferCheckpointTurnCountByTurnId(  summaries: TurnDiffSummary[],): Record<TurnId, number> {  const sorted = [...summaries].toSorted((a, b) => a.completedAt.localeCompare(b.completedAt));  const result: Record<TurnId, number> = {};  for (let index = 0; index < sorted.length; index += 1) {    const summary = sorted[index];    if (!summary) continue;    result[summary.turnId] = index + 1;  }  return result;} export function derivePhase(session: ThreadSession | null): SessionPhase {  if (!session || session.status === "closed") return "disconnected";  if (session.status === "connecting") return "connecting";  if (session.status === "running") return "running";  return "ready";}