import { ApprovalRequestId, type OrchestrationLatestTurn, 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 isLatestTurnSettled( latestTurn: Pick | null, session: Pick | null, ): boolean { if (!latestTurn?.startedAt) return false; if (!latestTurn.completedAt) return false; if (!session) return true; if (session.orchestrationStatus === "running") return false; return true; } export function derivePendingApprovals( activities: ReadonlyArray, ): PendingApproval[] { const openByRequestId = new Map(); 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) : 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, 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) : 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, 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 { const sorted = [...summaries].toSorted((a, b) => a.completedAt.localeCompare(b.completedAt)); const result: Record = {}; 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"; }