import { useEffect, useState } from "react"; import { useShallow } from "zustand/react/shallow"; 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); // 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) { checkImage(); // Reconcile project statuses against actual Docker container state, // then refresh the project list so the UI reflects reality. reconcileProjectStatuses().then((projects) => { setProjects(projects); }).catch(() => { // If reconciliation fails (e.g. Docker hiccup), just load from store refresh(); }); } else { setShowInstallDialog(true); stopPolling = startDockerPolling(); } }); 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 const homeProjectIds = tabOrder.filter(isHomeTab).map(tabKeyId); return (
Claude Code, sandboxed in a container.
Then start its container and press{" "} Ctrl+T {" "} to open a Claude terminal.
{showAdd &&