import { sessionDisplayStatus, type SessionDisplayStatus } from "../../timeline-model"; import type { RailSession as Session } from "./session-list-entry"; export const DEFAULT_VISIBLE_TREE_LEVELS = 3; export const MAX_VISUAL_TREE_DEPTH = 3; export function sessionStatusLabel(status: SessionDisplayStatus): string { switch (status) { case "blocked": return "Stuck"; case "requires_action": return "Needs you"; case "waiting_capacity": return "Limit reached"; case "recovering": return "Recovering"; case "running": return "Running"; case "queued": return "Queued"; case "failed": return "Failed"; case "cancelled": return "Cancelled"; default: return "Idle"; } } import { sessionHasUpcomingSchedule, sessionInputWait } from "../../session-list-model"; export { sessionHasUpcomingSchedule, sessionInputWait }; export function sessionWaitLabel(deadlineAt: string, now = Date.now(), compact = false): string { const deadline = new Date(deadlineAt); if (deadline.getTime() <= now) return compact ? "Recheck due" : "Waiting · recheck due"; const sameDay = deadline.toDateString() === new Date(now).toDateString(); const time = deadline.toLocaleString(undefined, { ...(sameDay ? {} : { month: "short", day: "numeric" }), hour: "2-digit", minute: "2-digit", }); return compact ? `Waiting · ${time}` : `Waiting · recheck at ${time}`; } /** Honest user-facing state: lifecycle first, then the effective pause policy. */ export function sessionStateLabel(session: Session): string { const commandActivity = session.backgroundCommandActivity; if (commandActivity?.unavailableCount) { const unavailable = Math.min(commandActivity.count, commandActivity.unavailableCount); const known = commandActivity.count - unavailable; const unknownLabel = unavailable === 1 ? "Command status unavailable" : `${unavailable} command statuses unavailable`; return known > 0 ? `${known} other active background command${known === 1 ? "" : "s"} · ${unknownLabel}` : commandActivity.state === "stopping" ? `Stop requested · ${unknownLabel}` : unknownLabel; } if (commandActivity?.state === "stopping") { return commandActivity.count === 1 ? "Stopping background command…" : `Stopping ${commandActivity.count} background commands…`; } if (commandActivity?.state === "running") { return commandActivity.count === 1 ? "Background command running" : `${commandActivity.count} background commands running`; } const waiting = sessionInputWait(session); const lifecycle = waiting ? sessionWaitLabel(waiting.deadlineAt, Date.now(), true) : sessionStatusLabel(sessionDisplayStatus(session)); const attentionOrTerminal = session.status === "requires_action" || session.status === "failed" || session.status === "cancelled"; const control = session.effectiveControl; if (control.state !== "paused") { return control.override ? `${lifecycle} · Resumed workstream` : lifecycle; } if (control.settlement) return "Pausing…"; const blocker = control.primaryBlocker; const pause = blocker?.kind === "workspace" ? "Workspace paused" : control.directState === "paused" || blocker?.sessionId === session.id ? "Paused here" : `Paused by ${blocker?.displayName ?? "parent"}`; return attentionOrTerminal ? `${lifecycle} · ${pause}` : pause; } /** Root-to-parent path for the URL-active session, guarded against corrupt cycles. */ export function sessionAncestorPath( activeSessionId: string | null, parentOf: ReadonlyMap, ): string[] { if (!activeSessionId) return []; const reversePath: string[] = []; const seen = new Set([activeSessionId]); let cursor = parentOf.get(activeSessionId); while (cursor && !seen.has(cursor)) { seen.add(cursor); reversePath.push(cursor); cursor = parentOf.get(cursor); } return reversePath.reverse(); } /** Expand only enough ancestors to show the default number of real tree levels. */ export function defaultExpandedAncestors( ancestorPath: readonly string[], manuallyCollapsed: ReadonlySet, visibleLevels = DEFAULT_VISIBLE_TREE_LEVELS, ): ReadonlySet { const expansionCount = Math.max(0, visibleLevels - 1); return new Set( ancestorPath.slice(0, expansionCount).filter((sessionId) => !manuallyCollapsed.has(sessionId)), ); } export function visualTreeDepth(depth: number): number { return Math.min(MAX_VISUAL_TREE_DEPTH, Math.max(0, depth)); } /** * Whether a session is held by a pause for display. Cancel is implemented as a terminal * status plus a pause fence, so a cancelled session must read "Cancelled", never "Paused". */ export function sessionControlPaused(session: { status: string; effectiveControl?: { state: string } | null | undefined; }): boolean { return session.status !== "cancelled" && session.effectiveControl?.state !== "active"; }