/* Copyright 2026 Marimo. All rights reserved. */ import { useAtomValue } from "jotai"; import { ArrowRightIcon, ChevronRightIcon } from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { hasCellsAtom } from "@/core/cells/cells"; import type { AppConfig } from "@/core/config/config-schema"; import { connectionAtom } from "@/core/network/connection"; import { useConnectionNotice } from "@/core/network/useConnectionNotice"; import { sandboxAtom } from "@/core/packages/sandbox-state"; import { WebSocketState } from "@/core/websocket/types"; import { useInterval } from "@/hooks/useInterval"; import { cn } from "@/utils/cn"; import { SandboxErrorOutput } from "../chrome/panels/sandbox-panel"; import { chromeAtom, useChromeActions } from "../chrome/state"; import { VerticalLayoutWrapper } from "../renderers/vertical-layout/vertical-layout-wrapper"; import { ConnectionStatusIcon, StartupProgress } from "./startup-progress"; export const ConnectionNotice = ({ appConfig, onRetry, }: { appConfig: AppConfig; onRetry: () => void; }) => { const notice = useConnectionNotice(); const connection = useAtomValue(connectionAtom); const hasCells = useAtomValue(hasCellsAtom); const sandbox = useAtomValue(sandboxAtom); const chrome = useAtomValue(chromeAtom); const packagesOpen = (chrome.isSidebarOpen && chrome.selectedPanel === "packages") || (chrome.isDeveloperPanelOpen && chrome.selectedDeveloperPanelTab === "packages"); const { openApplication } = useChromeActions(); // Reconnection uses the footer indicator while existing cells stay visible. const reconnecting = connection.state === WebSocketState.CONNECTING && connection.phase === "reconnecting"; if (sandbox?.backend && hasCells) { const title = notice?.ready && notice.kind === "startup" ? "Ready" : notice?.title; return ( {notice && !reconnecting && ( openApplication("packages")} aria-label={`${title} Open Packages`} aria-expanded={packagesOpen} className="flex h-6 items-center gap-2 whitespace-nowrap text-xs text-muted-foreground hover:text-foreground rounded focus-visible:outline-2 focus-visible:outline-ring" > {title} )} ); } if (!notice || (hasCells && reconnecting)) { return null; } const showSteps = notice.sandbox && notice.kind === "startup" && (notice.ready || notice.phase === "preparing-environment" || notice.phase === "starting-kernel"); return ( {showSteps ? ( ) : ( {notice.title} )} {!showSteps && ( {notice.description} )} {!notice.sandbox && notice.error && ( )} {!notice.ready && !packagesOpen && (notice.sandbox || notice.pending) && ( openApplication("packages")} > {notice.pending ? "View setup details" : "Open Packages"} )} {!notice.pending && !notice.sandbox && ( Try again )} {notice.pending && ( )} ); }; function StartupElapsedTime() { const [startedAt] = useState(Date.now); const [seconds, setSeconds] = useState(0); useInterval(() => setSeconds(Math.floor((Date.now() - startedAt) / 1000)), { delayMs: 1000, whenVisible: true, }); const duration = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`; return ( Elapsed {duration} ); }
{notice.description}