import type { StatusTone } from "../../components/StatusPill"; import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts"; import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; export function threadSortValue(thread: EnvironmentThreadShell): number { const candidate = Date.parse(thread.updatedAt ?? thread.createdAt); return Number.isNaN(candidate) ? 0 : candidate; } export type ThreadStatusKind = | "pending-approval" | "awaiting-input" | "working" | "connecting" | "error" | "plan-ready"; export interface ThreadStatusPresentation extends StatusTone { readonly kind: ThreadStatusKind; /** Foreground color for the leading status icon. */ readonly iconColor: string; /** Background color for the leading status icon circle. */ readonly iconBackground: string; /** Whether the indicator represents in-flight activity. */ readonly pulse: boolean; } /** Neutral icon colors for threads with no actionable status. */ export const THREAD_STATUS_NEUTRAL_ICON = { iconColor: "#8e8e93", iconBackground: "rgba(142,142,147,0.22)", } as const; function isLatestTurnSettled( latestTurn: OrchestrationLatestTurn | null, session: OrchestrationSession | null, ): boolean { if (!latestTurn?.startedAt) return false; if (!latestTurn.completedAt) return false; if (!session) return true; return session.status !== "running"; } /** * Resolves the user-facing status of a thread, in priority order. Returns * `null` for quiescent threads so rows stay free of "Idle"-style noise. * Mirrors `resolveThreadStatusPill` in apps/web/src/components/Sidebar.logic.ts. */ export function resolveThreadStatus( thread: EnvironmentThreadShell, ): ThreadStatusPresentation | null { if (thread.hasPendingApprovals) { return { kind: "pending-approval", label: "Needs Approval", pillClassName: "bg-amber-500/12 dark:bg-amber-500/16", textClassName: "text-amber-700 dark:text-amber-300", iconColor: "#ff9f0a", iconBackground: "rgba(255,159,10,0.22)", pulse: false, }; } if (thread.hasPendingUserInput) { return { kind: "awaiting-input", label: "Awaiting Input", pillClassName: "bg-indigo-500/12 dark:bg-indigo-500/16", textClassName: "text-indigo-700 dark:text-indigo-300", iconColor: "#5e5ce6", iconBackground: "rgba(94,92,230,0.22)", pulse: false, }; } if (thread.session?.status === "running") { return { kind: "working", label: "Working", pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", textClassName: "text-sky-700 dark:text-sky-300", iconColor: "#0a84ff", iconBackground: "rgba(10,132,255,0.22)", pulse: true, }; } if (thread.session?.status === "starting") { return { kind: "connecting", label: "Connecting", pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", textClassName: "text-sky-700 dark:text-sky-300", iconColor: "#0a84ff", iconBackground: "rgba(10,132,255,0.22)", pulse: true, }; } if (thread.session?.status === "error" || thread.latestTurn?.state === "error") { return { kind: "error", label: "Error", pillClassName: "bg-rose-500/12 dark:bg-rose-500/16", textClassName: "text-rose-700 dark:text-rose-300", iconColor: "#ff453a", iconBackground: "rgba(255,69,58,0.22)", pulse: false, }; } const hasPlanReadyPrompt = thread.interactionMode === "plan" && isLatestTurnSettled(thread.latestTurn, thread.session) && thread.hasActionableProposedPlan; if (hasPlanReadyPrompt) { return { kind: "plan-ready", label: "Plan Ready", pillClassName: "bg-violet-500/12 dark:bg-violet-500/16", textClassName: "text-violet-700 dark:text-violet-300", iconColor: "#bf5af2", iconBackground: "rgba(191,90,242,0.22)", pulse: false, }; } return null; }