import { useState } from "react"; import type { Session } from "../api/sessions"; import { Icon } from "./icon"; import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { MobileActionSheet } from "./mobile-action-sheet"; import type { IconName } from "./icon"; export interface SessionCardProps { session: Session; onOpen?: (session: Session) => void; onStart?: (session: Session) => void; onStop?: (session: Session) => void; onDelete?: (session: Session) => void; onRecreateTunnel?: (session: Session) => void; isBusy?: boolean; tunnelHealth?: { healthy: boolean; container_status: string; container_health: string | null; tunnel_status: string; tunnel_status_code: number | null; probe_status: string; last_probe_output: string | null; error: string | null; } | null; } const statusConfig: Record = { running: { color: "green", label: "Running" }, building: { color: "yellow", label: "Building" }, starting: { color: "yellow", label: "Starting" }, probing: { color: "yellow", label: "Probing" }, pending: { color: "yellow", label: "Pending" }, stopped: { color: "gray", label: "Stopped" }, error: { color: "red", label: "Error" }, unhealthy: { color: "orange", label: "Unhealthy" }, }; export function SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, isBusy = false, tunnelHealth = null, }: SessionCardProps) { const [showStopConfirm, setShowStopConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showActionSheet, setShowActionSheet] = useState(false); const isMobile = useMobileViewport(); const status = statusConfig[session.status] || { color: "gray", label: session.status }; const isTerminalOnly = session.tool_type_interfaces?.includes("terminal") && !session.tool_type_interfaces?.includes("web"); const hasTunnelError = !isTerminalOnly && tunnelHealth?.tunnel_status === "unreachable"; const hasAppError = !isTerminalOnly && tunnelHealth?.tunnel_status === "error_response"; const handleStop = () => { if (showStopConfirm) { setShowStopConfirm(false); onStop?.(session); } else { setShowStopConfirm(true); } }; const handleDelete = () => { if (showDeleteConfirm) { setShowDeleteConfirm(false); onDelete?.(session); } else { setShowDeleteConfirm(true); } }; const handleCancelStop = () => setShowStopConfirm(false); const handleCancelDelete = () => setShowDeleteConfirm(false); const isActive = ["running", "building", "starting", "probing", "pending", "unhealthy"].includes(session.status); return (
{isBusy && (
)}

{session.display_name}

{status.label} {hasTunnelError && ( Tunnel Error )} {hasAppError && ( App Error {tunnelHealth?.tunnel_status_code} )}

{session.tool_type_name} {session.project_name && ` · ${session.project_name}`} {session.repository_name && ` · ${session.repository_name}`}

{session.clone_mode && (

{session.clone_mode === "clone" ? `Clone${session.branch ? ` (${session.branch})` : ""}` : "Mount"}

)} {session.url && (

{session.url}

)} {session.created_at && (

Created: {new Date(session.created_at).toLocaleString()}

)}
{isMobile ? (
{isActive && ( <> {session.url ? ( Open ) : ( )} )} {!isActive && onStart && ( )} {!isActive && ( )}
) : (
{isActive && ( <> {session.url ? ( Open ) : ( )} {hasTunnelError && onRecreateTunnel && ( )} {showStopConfirm ? (
Stop?
) : ( )} )} {!isActive && onStart && ( )} {showDeleteConfirm ? (
Delete?
) : ( )}
)} setShowActionSheet(false)} title={session.display_name} actions={[ ...(isActive && hasTunnelError && onRecreateTunnel ? [{ id: "tunnel", label: "Recreate Tunnel", icon: "refresh" as IconName, onClick: () => onRecreateTunnel(session), }] : []), ...(isActive && onStop ? [{ id: "stop", label: "Stop", icon: "stop" as IconName, variant: "danger" as const, onClick: () => onStop(session), }] : []), ...(onDelete ? [{ id: "delete", label: "Delete", icon: "delete" as IconName, variant: "danger" as const, onClick: () => onDelete(session), }] : []), ]} />
); }