import { ApprovalRequestId, type OrchestrationLatestTurn, type OrchestrationThreadActivity, type OrchestrationProposedPlanId, type ProviderKind, type UserInputQuestion, type TurnId,} from "@t3tools/contracts"; import type { ChatMessage, ProposedPlan, SessionPhase, ThreadSession, TurnDiffSummary } from "./types"; export type ProviderPickerKind = ProviderKind | "claudeCode" | "cursor"; export const PROVIDER_OPTIONS: Array<{ value: ProviderPickerKind; label: string; available: boolean;}> = [ { value: "codex", label: "Codex", available: true }, { value: "claudeCode", label: "Claude Code", available: false }, { value: "cursor", label: "Cursor Agent", available: true },]; export interface WorkLogEntry { id: string; createdAt: string; label: string; detail?: string; tone: "thinking" | "tool" | "info" | "error";} export interface PendingApproval { requestId: ApprovalRequestId; requestKind: "command" | "file-read" | "file-change"; createdAt: string; detail?: string;} export interface PendingUserInput { requestId: ApprovalRequestId; createdAt: string; questions: ReadonlyArray<UserInputQuestion>;} export interface ActivePlanState { createdAt: string; turnId: TurnId | null; explanation?: string | null; steps: Array<{ step: string; status: "pending" | "inProgress" | "completed"; }>;} export interface LatestProposedPlanState { id: OrchestrationProposedPlanId; createdAt: string; updatedAt: string; turnId: TurnId | null; planMarkdown: string;} export type TimelineEntry = | { id: string; kind: "message"; createdAt: string; message: ChatMessage; } | { id: string; kind: "proposed-plan"; createdAt: string; proposedPlan: ProposedPlan; } | { 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<OrchestrationLatestTurn, "turnId" | "startedAt" | "completedAt"> | null, session: Pick<ThreadSession, "orchestrationStatus" | "activeTurnId"> | null,): boolean { if (!latestTurn?.startedAt) return false; if (!latestTurn.completedAt) return false; if (!session) return true; if (session.orchestrationStatus === "running") return false; return true;} function requestKindFromRequestType( requestType: unknown,): PendingApproval["requestKind"] | null { switch (requestType) { case "command_execution_approval": case "exec_command_approval": return "command"; case "file_read_approval": return "file-read"; case "file_change_approval": case "apply_patch_approval": return "file-change"; default: return null; }} export function derivePendingApprovals( activities: ReadonlyArray<OrchestrationThreadActivity>,): PendingApproval[] { const openByRequestId = new Map<ApprovalRequestId, PendingApproval>(); const ordered = [...activities].toSorted(compareActivitiesByOrder); 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-read" || payload.requestKind === "file-change") ? payload.requestKind : payload ? requestKindFromRequestType(payload.requestType) : 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); continue; } if ( activity.kind === "provider.approval.respond.failed" && requestId && detail?.includes("Unknown pending permission request") ) { openByRequestId.delete(requestId); continue; } } return [...openByRequestId.values()].toSorted((left, right) => left.createdAt.localeCompare(right.createdAt), );} function parseUserInputQuestions( payload: Record<string, unknown> | null,): ReadonlyArray<UserInputQuestion> | null { const questions = payload?.questions; if (!Array.isArray(questions)) { return null; } const parsed = questions .map<UserInputQuestion | null>((entry) => { if (!entry || typeof entry !== "object") return null; const question = entry as Record<string, unknown>; if ( typeof question.id !== "string" || typeof question.header !== "string" || typeof question.question !== "string" || !Array.isArray(question.options) ) { return null; } const options = question.options .map<UserInputQuestion["options"][number] | null>((option) => { if (!option || typeof option !== "object") return null; const optionRecord = option as Record<string, unknown>; if ( typeof optionRecord.label !== "string" || typeof optionRecord.description !== "string" ) { return null; } return { label: optionRecord.label, description: optionRecord.description, }; }) .filter((option): option is UserInputQuestion["options"][number] => option !== null); if (options.length === 0) { return null; } return { id: question.id, header: question.header, question: question.question, options, }; }) .filter((question): question is UserInputQuestion => question !== null); return parsed.length > 0 ? parsed : null;} export function derivePendingUserInputs( activities: ReadonlyArray<OrchestrationThreadActivity>,): PendingUserInput[] { const openByRequestId = new Map<ApprovalRequestId, PendingUserInput>(); const ordered = [...activities].toSorted(compareActivitiesByOrder); 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; if (activity.kind === "user-input.requested" && requestId) { const questions = parseUserInputQuestions(payload); if (!questions) { continue; } openByRequestId.set(requestId, { requestId, createdAt: activity.createdAt, questions, }); continue; } if (activity.kind === "user-input.resolved" && requestId) { openByRequestId.delete(requestId); } } return [...openByRequestId.values()].toSorted((left, right) => left.createdAt.localeCompare(right.createdAt), );} export function deriveActivePlanState( activities: ReadonlyArray<OrchestrationThreadActivity>, latestTurnId: TurnId | undefined,): ActivePlanState | null { const ordered = [...activities].toSorted(compareActivitiesByOrder); const candidates = ordered.filter((activity) => { if (activity.kind !== "turn.plan.updated") { return false; } if (!latestTurnId) { return true; } return activity.turnId === latestTurnId; }); const latest = candidates.at(-1); if (!latest) { return null; } const payload = latest.payload && typeof latest.payload === "object" ? (latest.payload as Record<string, unknown>) : null; const rawPlan = payload?.plan; if (!Array.isArray(rawPlan)) { return null; } const steps = rawPlan .map((entry) => { if (!entry || typeof entry !== "object") return null; const record = entry as Record<string, unknown>; if (typeof record.step !== "string") { return null; } const status = record.status === "completed" || record.status === "inProgress" ? record.status : "pending"; return { step: record.step, status, }; }) .filter( ( step, ): step is { step: string; status: "pending" | "inProgress" | "completed"; } => step !== null, ); if (steps.length === 0) { return null; } return { createdAt: latest.createdAt, turnId: latest.turnId, ...(payload && "explanation" in payload ? { explanation: payload.explanation as string | null } : {}), steps, };} export function findLatestProposedPlan( proposedPlans: ReadonlyArray<ProposedPlan>, latestTurnId: TurnId | string | null | undefined,): LatestProposedPlanState | null { if (latestTurnId) { const matchingTurnPlan = [...proposedPlans] .filter((proposedPlan) => proposedPlan.turnId === latestTurnId) .toSorted( (left, right) => left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id), ) .at(-1); if (matchingTurnPlan) { return { id: matchingTurnPlan.id, createdAt: matchingTurnPlan.createdAt, updatedAt: matchingTurnPlan.updatedAt, turnId: matchingTurnPlan.turnId, planMarkdown: matchingTurnPlan.planMarkdown, }; } } const latestPlan = [...proposedPlans] .toSorted( (left, right) => left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id), ) .at(-1); if (!latestPlan) { return null; } return { id: latestPlan.id, createdAt: latestPlan.createdAt, updatedAt: latestPlan.updatedAt, turnId: latestPlan.turnId, planMarkdown: latestPlan.planMarkdown, };} export function deriveWorkLogEntries( activities: ReadonlyArray<OrchestrationThreadActivity>, latestTurnId: TurnId | undefined,): WorkLogEntry[] { const ordered = [...activities].toSorted(compareActivitiesByOrder); 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; });} function compareActivitiesByOrder( left: OrchestrationThreadActivity, right: OrchestrationThreadActivity,): number { if (left.sequence !== undefined && right.sequence !== undefined) { if (left.sequence !== right.sequence) { return left.sequence - right.sequence; } } else if (left.sequence !== undefined) { return 1; } else if (right.sequence !== undefined) { return -1; } return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);} 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[], proposedPlans: ProposedPlan[], workEntries: WorkLogEntry[],): TimelineEntry[] { const messageRows: TimelineEntry[] = messages.map((message) => ({ id: message.id, kind: "message", createdAt: message.createdAt, message, })); const proposedPlanRows: TimelineEntry[] = proposedPlans.map((proposedPlan) => ({ id: proposedPlan.id, kind: "proposed-plan", createdAt: proposedPlan.createdAt, proposedPlan, })); const workRows: TimelineEntry[] = workEntries.map((entry) => ({ id: entry.id, kind: "work", createdAt: entry.createdAt, entry, })); return [...messageRows, ...proposedPlanRows, ...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";}
import {
ApprovalRequestId,
type OrchestrationLatestTurn,
type OrchestrationThreadActivity,
type OrchestrationProposedPlanId,
type ProviderKind,
type UserInputQuestion,
type TurnId,
} from "@t3tools/contracts";
import type { ChatMessage, ProposedPlan, SessionPhase, ThreadSession, TurnDiffSummary } from "./types";
export type ProviderPickerKind = ProviderKind | "claudeCode" | "cursor";
export const PROVIDER_OPTIONS: Array<{
value: ProviderPickerKind;
label: string;
available: boolean;
}> = [
{ value: "codex", label: "Codex", available: true },
{ value: "claudeCode", label: "Claude Code", available: false },
{ value: "cursor", label: "Cursor Agent", available: true },
];
export interface WorkLogEntry {
id: string;
createdAt: string;
label: string;
detail?: string;
tone: "thinking" | "tool" | "info" | "error";
}
export interface PendingApproval {
requestId: ApprovalRequestId;
requestKind: "command" | "file-read" | "file-change";
createdAt: string;
detail?: string;
}
export interface PendingUserInput {
requestId: ApprovalRequestId;
createdAt: string;
questions: ReadonlyArray<UserInputQuestion>;
}
export interface ActivePlanState {
createdAt: string;
turnId: TurnId | null;
explanation?: string | null;
steps: Array<{
step: string;
status: "pending" | "inProgress" | "completed";
}>;
}
export interface LatestProposedPlanState {
id: OrchestrationProposedPlanId;
createdAt: string;
updatedAt: string;
turnId: TurnId | null;
planMarkdown: string;
}
export type TimelineEntry =
| {
id: string;
kind: "message";
createdAt: string;
message: ChatMessage;
}
| {
id: string;
kind: "proposed-plan";
createdAt: string;
proposedPlan: ProposedPlan;
}
| {
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<OrchestrationLatestTurn, "turnId" | "startedAt" | "completedAt"> | null,
session: Pick<ThreadSession, "orchestrationStatus" | "activeTurnId"> | null,
): boolean {
if (!latestTurn?.startedAt) return false;
if (!latestTurn.completedAt) return false;
if (!session) return true;
if (session.orchestrationStatus === "running") return false;
return true;
}
function requestKindFromRequestType(
requestType: unknown,
): PendingApproval["requestKind"] | null {
switch (requestType) {
case "command_execution_approval":
case "exec_command_approval":
return "command";
case "file_read_approval":
return "file-read";
case "file_change_approval":
case "apply_patch_approval":
return "file-change";
default:
return null;
}
}
export function derivePendingApprovals(
activities: ReadonlyArray<OrchestrationThreadActivity>,
): PendingApproval[] {
const openByRequestId = new Map<ApprovalRequestId, PendingApproval>();
const ordered = [...activities].toSorted(compareActivitiesByOrder);
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-read" ||
payload.requestKind === "file-change")
? payload.requestKind
: payload
? requestKindFromRequestType(payload.requestType)
: 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);
continue;
}
if (
activity.kind === "provider.approval.respond.failed" &&
requestId &&
detail?.includes("Unknown pending permission request")
) {
openByRequestId.delete(requestId);
continue;
}
}
return [...openByRequestId.values()].toSorted((left, right) =>
left.createdAt.localeCompare(right.createdAt),
);
}
function parseUserInputQuestions(
payload: Record<string, unknown> | null,
): ReadonlyArray<UserInputQuestion> | null {
const questions = payload?.questions;
if (!Array.isArray(questions)) {
return null;
}
const parsed = questions
.map<UserInputQuestion | null>((entry) => {
if (!entry || typeof entry !== "object") return null;
const question = entry as Record<string, unknown>;
if (
typeof question.id !== "string" ||
typeof question.header !== "string" ||
typeof question.question !== "string" ||
!Array.isArray(question.options)
) {
return null;
}
const options = question.options
.map<UserInputQuestion["options"][number] | null>((option) => {
if (!option || typeof option !== "object") return null;
const optionRecord = option as Record<string, unknown>;
if (
typeof optionRecord.label !== "string" ||
typeof optionRecord.description !== "string"
) {
return null;
}
return {
label: optionRecord.label,
description: optionRecord.description,
};
})
.filter((option): option is UserInputQuestion["options"][number] => option !== null);
if (options.length === 0) {
return null;
}
return {
id: question.id,
header: question.header,
question: question.question,
options,
};
})
.filter((question): question is UserInputQuestion => question !== null);
return parsed.length > 0 ? parsed : null;
}
export function derivePendingUserInputs(
activities: ReadonlyArray<OrchestrationThreadActivity>,
): PendingUserInput[] {
const openByRequestId = new Map<ApprovalRequestId, PendingUserInput>();
const ordered = [...activities].toSorted(compareActivitiesByOrder);
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;
if (activity.kind === "user-input.requested" && requestId) {
const questions = parseUserInputQuestions(payload);
if (!questions) {
continue;
}
openByRequestId.set(requestId, {
requestId,
createdAt: activity.createdAt,
questions,
});
continue;
}
if (activity.kind === "user-input.resolved" && requestId) {
openByRequestId.delete(requestId);
}
}
return [...openByRequestId.values()].toSorted((left, right) =>
left.createdAt.localeCompare(right.createdAt),
);
}
export function deriveActivePlanState(
activities: ReadonlyArray<OrchestrationThreadActivity>,
latestTurnId: TurnId | undefined,
): ActivePlanState | null {
const ordered = [...activities].toSorted(compareActivitiesByOrder);
const candidates = ordered.filter((activity) => {
if (activity.kind !== "turn.plan.updated") {
return false;
}
if (!latestTurnId) {
return true;
}
return activity.turnId === latestTurnId;
});
const latest = candidates.at(-1);
if (!latest) {
return null;
}
const payload =
latest.payload && typeof latest.payload === "object"
? (latest.payload as Record<string, unknown>)
: null;
const rawPlan = payload?.plan;
if (!Array.isArray(rawPlan)) {
return null;
}
const steps = rawPlan
.map((entry) => {
if (!entry || typeof entry !== "object") return null;
const record = entry as Record<string, unknown>;
if (typeof record.step !== "string") {
return null;
}
const status =
record.status === "completed" || record.status === "inProgress"
? record.status
: "pending";
return {
step: record.step,
status,
};
})
.filter(
(
step,
): step is {
step: string;
status: "pending" | "inProgress" | "completed";
} => step !== null,
);
if (steps.length === 0) {
return null;
}
return {
createdAt: latest.createdAt,
turnId: latest.turnId,
...(payload && "explanation" in payload ? { explanation: payload.explanation as string | null } : {}),
steps,
};
}
export function findLatestProposedPlan(
proposedPlans: ReadonlyArray<ProposedPlan>,
latestTurnId: TurnId | string | null | undefined,
): LatestProposedPlanState | null {
if (latestTurnId) {
const matchingTurnPlan = [...proposedPlans]
.filter((proposedPlan) => proposedPlan.turnId === latestTurnId)
.toSorted(
(left, right) =>
left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id),
)
.at(-1);
if (matchingTurnPlan) {
return {
id: matchingTurnPlan.id,
createdAt: matchingTurnPlan.createdAt,
updatedAt: matchingTurnPlan.updatedAt,
turnId: matchingTurnPlan.turnId,
planMarkdown: matchingTurnPlan.planMarkdown,
};
}
}
const latestPlan = [...proposedPlans]
.toSorted(
(left, right) =>
left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id),
)
.at(-1);
if (!latestPlan) {
return null;
}
return {
id: latestPlan.id,
createdAt: latestPlan.createdAt,
updatedAt: latestPlan.updatedAt,
turnId: latestPlan.turnId,
planMarkdown: latestPlan.planMarkdown,
};
}
export function deriveWorkLogEntries(
activities: ReadonlyArray<OrchestrationThreadActivity>,
latestTurnId: TurnId | undefined,
): WorkLogEntry[] {
const ordered = [...activities].toSorted(compareActivitiesByOrder);
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;
});
}
function compareActivitiesByOrder(
left: OrchestrationThreadActivity,
right: OrchestrationThreadActivity,
): number {
if (left.sequence !== undefined && right.sequence !== undefined) {
if (left.sequence !== right.sequence) {
return left.sequence - right.sequence;
}
} else if (left.sequence !== undefined) {
return 1;
} else if (right.sequence !== undefined) {
return -1;
}
return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
}
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[],
proposedPlans: ProposedPlan[],
workEntries: WorkLogEntry[],
): TimelineEntry[] {
const messageRows: TimelineEntry[] = messages.map((message) => ({
id: message.id,
kind: "message",
createdAt: message.createdAt,
message,
}));
const proposedPlanRows: TimelineEntry[] = proposedPlans.map((proposedPlan) => ({
id: proposedPlan.id,
kind: "proposed-plan",
createdAt: proposedPlan.createdAt,
proposedPlan,
}));
const workRows: TimelineEntry[] = workEntries.map((entry) => ({
id: entry.id,
kind: "work",
createdAt: entry.createdAt,
entry,
}));
return [...messageRows, ...proposedPlanRows, ...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";
}