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;}
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;
}