import type { ThreadLinkedPullRequest, EnvironmentId, MessageId, OrchestrationProjectShell, OrchestrationV2RunStatus, OrchestrationV2ThreadProjection, OrchestrationV2ThreadShell, PlanId, ProjectId, ProviderInstanceId, RunId, ThreadId, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; export interface EnvironmentProject extends OrchestrationProjectShell { readonly environmentId: EnvironmentId; } /** * A pristine V2 thread projection paired with the environment that produced it. * * The projection stays nested so the server projection and all structurally * shared collections retain their identities. Rich consumers read V2 state * directly instead of a second presentation-shaped thread graph. */ export interface EnvironmentThread { readonly environmentId: EnvironmentId; readonly projection: OrchestrationV2ThreadProjection; } export interface ThreadRunSummary { readonly runId: RunId; readonly status: OrchestrationV2RunStatus; readonly requestedAt: string | null; readonly startedAt: string | null; readonly completedAt: string | null; readonly assistantMessageId: MessageId | null; readonly sourcePlanRef?: { readonly threadId: ThreadId; readonly planId: PlanId; }; } export interface ThreadRuntimeSummary { readonly status: OrchestrationV2RunStatus | "idle"; readonly activeRunId: RunId | null; readonly providerInstanceId: ProviderInstanceId; readonly providerName: string | null; readonly lastError: string | null; readonly updatedAt: string; } export function threadRuntimeIsActive(runtime: ThreadRuntimeSummary | null | undefined): boolean { return runtime !== null && runtime !== undefined && threadRunStatusIsActive(runtime.status); } export function threadRunStatusIsActive(status: ThreadRuntimeSummary["status"]): boolean { return ( status === "preparing" || status === "queued" || status === "starting" || status === "running" || status === "waiting" ); } export interface EnvironmentThreadShell { readonly environmentId: EnvironmentId; readonly id: ThreadId; readonly projectId: ProjectId; readonly title: string; readonly providerInstanceId: ProviderInstanceId; readonly modelSelection: OrchestrationV2ThreadShell["modelSelection"]; readonly runtimeMode: OrchestrationV2ThreadShell["runtimeMode"]; readonly interactionMode: OrchestrationV2ThreadShell["interactionMode"]; readonly branch: string | null; readonly worktreePath: string | null; readonly lineage: OrchestrationV2ThreadShell["lineage"]; readonly forkedFrom: OrchestrationV2ThreadShell["forkedFrom"]; readonly activeProviderThreadId: OrchestrationV2ThreadShell["activeProviderThreadId"]; readonly latestRun: ThreadRunSummary | null; readonly runtime: ThreadRuntimeSummary | null; readonly latestUserMessageAt: string | null; readonly hasPendingApprovals: boolean; readonly hasPendingUserInput: boolean; readonly hasActionableProposedPlan: boolean; readonly pendingBackgroundTasks: ReadonlyArray< NonNullable[number] >; readonly itemCount: number; readonly visibleItemCount: number; readonly createdAt: string; readonly updatedAt: string; readonly archivedAt: string | null; readonly settledOverride: "settled" | "active" | null; readonly settledAt: string | null; readonly snoozedUntil: string | null; readonly snoozedAt: string | null; readonly pinnedAt: string | null; /** Slot in the user-arranged pinned order; null for keyless (legacy) pins. */ readonly pinOrderKey: string | null; /** * Pull request the user linked to the thread (#8160). The v2 server does not * project this yet, so it stays undefined on v2 environments; UI treats * undefined and null alike. */ readonly linkedPullRequest?: ThreadLinkedPullRequest | null; /** * Server-tracked visited watermark. `undefined` means the environment's * server predates visited tracking and clients should fall back to any * local visited state they keep. */ readonly lastVisitedAt?: string | null; /** Pending title regeneration marker; null when no request is in flight. */ readonly titleRegeneration?: { readonly requestId: string; readonly startedAt: string } | null; readonly deletedAt: string | null; readonly source: OrchestrationV2ThreadShell; } function iso(value: DateTime.Utc): string { return DateTime.formatIso(value); } function nullableIso(value: DateTime.Utc | null): string | null { return value === null ? null : iso(value); } function terminalRunStatus(status: OrchestrationV2RunStatus): boolean { return ( status === "completed" || status === "interrupted" || status === "failed" || status === "cancelled" || status === "rolled_back" ); } // Park runtime at idle when the post-settlement background roster is nonempty // so #4415 waiting-presentation Waiting (session.idle) can consume CTM runtime. // The server suppresses the roster while an interruptible activity run exists, // so a remaining roster is stronger than checkpoint-oriented waiting. // latestRun keeps the latest run's status for history presentation. function shellRuntime(thread: OrchestrationV2ThreadShell): ThreadRuntimeSummary | null { if (thread.latestRunId === null && thread.activeProviderThreadId === null) return null; const hasPendingBackgroundTasks = (thread.pendingBackgroundTasks?.length ?? 0) > 0; const status = hasPendingBackgroundTasks ? "idle" : (thread.activityRunStatus ?? thread.status); return { status, activeRunId: thread.activeRunId, providerInstanceId: thread.providerInstanceId, providerName: null, lastError: thread.lastError ?? null, updatedAt: iso(thread.updatedAt), }; } export function scopeProject( environmentId: EnvironmentId, project: OrchestrationProjectShell, ): EnvironmentProject { return { ...project, environmentId }; } export function presentThreadShell( environmentId: EnvironmentId, thread: OrchestrationV2ThreadShell, ): EnvironmentThreadShell { const updatedAt = iso(thread.updatedAt); const latestRun = thread.latestRunId === null ? null : ({ runId: thread.latestRunId, status: thread.status === "idle" ? "completed" : thread.status, requestedAt: nullableIso(thread.latestRunRequestedAt ?? null), startedAt: nullableIso(thread.latestRunStartedAt ?? null), completedAt: thread.latestRunCompletedAt === undefined ? thread.status === "idle" || terminalRunStatus(thread.status) ? updatedAt : null : nullableIso(thread.latestRunCompletedAt), assistantMessageId: null, } satisfies ThreadRunSummary); return { environmentId, id: thread.id, projectId: thread.projectId, title: thread.title, providerInstanceId: thread.providerInstanceId, modelSelection: thread.modelSelection, runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, branch: thread.branch, worktreePath: thread.worktreePath, linkedPullRequest: thread.linkedPullRequest ?? null, lineage: thread.lineage, forkedFrom: thread.forkedFrom, activeProviderThreadId: thread.activeProviderThreadId, latestRun, runtime: shellRuntime(thread), latestUserMessageAt: nullableIso(thread.latestUserMessageAt), hasPendingApprovals: thread.pendingRuntimeRequest !== null && thread.pendingRuntimeRequest.kind !== "user_input" && thread.pendingRuntimeRequest.kind !== "auth_refresh", hasPendingUserInput: thread.pendingRuntimeRequest?.kind === "user_input", hasActionableProposedPlan: thread.hasActionableProposedPlan, pendingBackgroundTasks: thread.pendingBackgroundTasks ?? [], itemCount: thread.itemCount, visibleItemCount: thread.visibleItemCount, createdAt: iso(thread.createdAt), updatedAt, archivedAt: nullableIso(thread.archivedAt), settledOverride: thread.settledOverride, settledAt: nullableIso(thread.settledAt), snoozedUntil: nullableIso(thread.snoozedUntil ?? null), snoozedAt: nullableIso(thread.snoozedAt ?? null), pinnedAt: nullableIso(thread.pinnedAt ?? null), pinOrderKey: thread.pinOrderKey ?? null, ...(thread.lastVisitedAt === undefined ? {} : { lastVisitedAt: nullableIso(thread.lastVisitedAt) }), titleRegeneration: thread.titleRegeneration == null ? null : { requestId: thread.titleRegeneration.requestId, startedAt: iso(thread.titleRegeneration.startedAt), }, deletedAt: nullableIso(thread.deletedAt), source: thread, }; } export const scopeThreadShell = presentThreadShell;