import { useState, useRef, useEffect } from "react"; import type { Session } from "../../../api/sessions"; import { Icon } from "../../icon"; import { useMobileViewport } from "../../../hooks/use-mobile-viewport"; import { MobileActionSheet } from "../mobile/mobile-action-sheet"; import { LoadingOverlay } from "../../loading-overlay"; 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; onRename?: (session: Session, newName: string) => void; isBusy?: boolean; busyLabel?: string; 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: "running", label: "Running" }, building: { color: "pending", label: "Building" }, starting: { color: "starting", label: "Starting" }, probing: { color: "probing", label: "Probing" }, pending: { color: "pending", label: "Pending" }, stopped: { color: "stopped", label: "Stopped" }, error: { color: "error", label: "Error" }, unhealthy: { color: "unhealthy", label: "Unhealthy" }, }; export function SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, isBusy = false, busyLabel = "Working...", tunnelHealth = null, }: SessionCardProps) { const [showActionSheet, setShowActionSheet] = useState(false); const [isEditingName, setIsEditingName] = useState(false); const [editName, setEditName] = useState(session.display_name); const [optionsOpen, setOptionsOpen] = useState(false); const optionsRef = useRef(null); 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 isActive = [ "running", "building", "starting", "probing", "pending", "unhealthy", ].includes(session.status); useEffect(() => { if (!optionsOpen) return; const handleClick = (e: MouseEvent) => { if ( optionsRef.current && !optionsRef.current.contains(e.target as Node) ) { setOptionsOpen(false); } }; const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOptionsOpen(false); }; document.addEventListener("mousedown", handleClick); document.addEventListener("keydown", handleKey); return () => { document.removeEventListener("mousedown", handleClick); document.removeEventListener("keydown", handleKey); }; }, [optionsOpen]); const handleDeleteClick = () => { setOptionsOpen(false); if (window.confirm("Are you sure you want to delete this session?")) { onDelete?.(session); } }; return (
{isEditingName ? (
setEditName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { onRename?.(session, editName); setIsEditingName(false); } if (e.key === "Escape") { setEditName(session.display_name); setIsEditingName(false); } }} onBlur={() => { if (editName.trim() && editName !== session.display_name) { onRename?.(session, editName); } setIsEditingName(false); }} autoFocus />
) : (

{ setEditName(session.display_name); setIsEditingName(true); }} title="Click to rename" style={{ cursor: "pointer" }} > {session.display_name}

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

{session.project_name} {session.workspace_name && ( <> {" "} / {session.workspace_name} )} {!session.workspace_name && session.repository_name && ( <> {" "} / {session.repository_name} )}{" "} ยท {session.tool_type_name}

{session.url && (

)} {session.created_at && (

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

)}
{isMobile ? (
{isActive && ( <> )} {!isActive && onStart && ( <> )}
) : (
{isActive && onOpen && ( )} {!isActive && onStart && ( )}
{optionsOpen && (
{isActive && onStop && ( )} {!isActive && onStart && ( )} {isActive && !isTerminalOnly && onRecreateTunnel && ( )} {onDelete && ( )}
)}
)} setShowActionSheet(false)} title={session.display_name} actions={[ ...(isActive && !isTerminalOnly && 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), }, ] : []), ...(!isActive && onStart ? [ { id: "start", label: "Start", icon: "play" as IconName, onClick: () => onStart(session), }, ] : []), ...(onDelete ? [ { id: "delete", label: "Delete", icon: "delete" as IconName, variant: "danger" as const, onClick: () => onDelete(session), }, ] : []), ]} />
); }