import { useCallback, useEffect, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { listen } from "@tauri-apps/api/event"; import Sidebar from "./components/layout/Sidebar"; import TopBar from "./components/layout/TopBar"; import StatusBar from "./components/layout/StatusBar"; import TerminalView from "./components/terminal/TerminalView"; import DockerInstallDialog from "./components/DockerInstallDialog"; import ProjectHome from "./components/projects/home/ProjectHome"; import AddProjectDialog from "./components/projects/AddProjectDialog"; import ToastHost from "./components/ui/ToastHost"; import StatusIndicator from "./components/ui/StatusIndicator"; import Button from "./components/ui/Button"; import { useDocker } from "./hooks/useDocker"; import { useSettings } from "./hooks/useSettings"; import { useProjects } from "./hooks/useProjects"; import { useUpdates } from "./hooks/useUpdates"; import { useTerminal } from "./hooks/useTerminal"; import { useSTT } from "./hooks/useSTT"; import { useContainerProgress } from "./hooks/useContainerProgress"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; import { useAppState, isHomeTab, tabKeyId, homeTabKey } from "./store/appState"; import { reconcileProjectStatuses } from "./lib/tauri-commands"; export default function App() { const { checkDocker, checkImage, startDockerPolling } = useDocker(); const { loadSettings } = useSettings(); const { refresh } = useProjects(); const { loadVersion, checkForUpdates, checkImageUpdate, startPeriodicCheck } = useUpdates(); const { sessions, activeSessionId, tabOrder, activeTabKey, setProjects, setSttToggle } = useAppState( useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, tabOrder: s.tabOrder, activeTabKey: s.activeTabKey, setProjects: s.setProjects, setSttToggle: s.setSttToggle, })) ); const [showInstallDialog, setShowInstallDialog] = useState(false); const [shuttingDown, setShuttingDown] = useState(false); /** * Everything that can only be done once Docker answers. Called from the * startup check *and* from the poller when the daemon shows up later — a * session that launched before Docker was ready otherwise never reconciles * container state or recovers an interrupted migration. */ const onDockerReady = useCallback(async () => { checkImage(); // Reconcile project statuses against actual Docker container state, // then refresh the project list so the UI reflects reality. try { setProjects(await reconcileProjectStatuses()); } catch { // If reconciliation fails (e.g. Docker hiccup), just load from store refresh(); } }, [checkImage, setProjects, refresh]); // Single STT instance bound to the active session. The mic lives in the // StatusBar; the terminal's Ctrl+Shift+M shortcut calls stt.toggle via the // store (registered below). const { sendInput } = useTerminal(); const stt = useSTT(activeSessionId ?? "", sendInput); useEffect(() => { setSttToggle(stt.toggle); }, [stt.toggle, setSttToggle]); useContainerProgress(); useKeyboardShortcuts(); // Initialize on mount useEffect(() => { loadSettings(); let stopPolling: (() => void) | undefined; checkDocker().then((available) => { if (available) { onDockerReady(); } else { setShowInstallDialog(true); stopPolling = startDockerPolling(onDockerReady); } }); refresh(); // Update detection loadVersion(); const updateTimer = setTimeout(() => { checkForUpdates(); checkImageUpdate(); }, 3000); const cleanup = startPeriodicCheck(); return () => { clearTimeout(updateTimer); cleanup?.(); stopPolling?.(); }; }, []); // eslint-disable-line react-hooks/exhaustive-deps // The backend prevents the window closing so it can stop containers first, // which freezes the UI for several seconds. This says why. useEffect(() => { let unlisten: (() => void) | undefined; let cancelled = false; listen("app-shutting-down", () => setShuttingDown(true)) .then((fn) => { if (cancelled) fn(); else unlisten = fn; }) .catch((e) => console.error("Failed to listen for shutdown:", e)); return () => { cancelled = true; unlisten?.(); }; }, []); const homeProjectIds = tabOrder.filter(isHomeTab).map(tabKeyId); return (
Stopping containers before quitting. This window will close on its own.
Claude Code, sandboxed in a container.
Then start its container and press{" "} Ctrl+T {" "} to open a Claude terminal.
{showAdd &&