import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; import { useAtomValue } from "@effect/atom-react"; import { useEffect } from "react"; import { isCommandPaletteOpen } from "../commandPaletteContext"; import { dispatchPreviewAction } from "../components/preview/previewActionBus"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { startNewLocalThreadFromContext, startNewThreadFromContext, } from "../lib/chatThreadActions"; import { isPreviewFocused } from "../lib/previewFocus"; import { isTerminalFocused } from "../lib/terminalFocus"; import { resolveShortcutCommand } from "../keybindings"; import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; import { isPreviewSupportedInRuntime } from "../previewStateStore"; import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { useThreadSelectionStore } from "../threadSelectionStore"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; import { primaryServerKeybindingsAtom } from "~/state/server"; function ChatRouteGlobalShortcuts() { const clearSelection = useThreadSelectionStore((state) => state.clearSelection); const selectedThreadKeysSize = useThreadSelectionStore((state) => state.selectedThreadKeys.size); const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread, routeThreadRef } = useHandleNewThread(); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const terminalOpen = useTerminalUiStateStore((state) => routeThreadRef ? selectThreadTerminalUiState(state.terminalUiStateByThreadKey, routeThreadRef).terminalOpen : false, ); // The `previewOpen` shortcut-context flag here uses the store-only value; // the URL-aware arbitration lives inside ChatView's `onTogglePreview`, // which we invoke via the action bus to avoid duplicating the rule. const previewOpen = useRightPanelStore((state) => routeThreadRef ? selectActiveRightPanel(state.byThreadKey, routeThreadRef) === "preview" : false, ); useEffect(() => { const onWindowKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; const command = resolveShortcutCommand(event, keybindings, { context: { terminalFocus: isTerminalFocused(), terminalOpen, previewFocus: isPreviewFocused(), previewOpen, }, }); if (isCommandPaletteOpen()) { return; } if (event.key === "Escape" && selectedThreadKeysSize > 0) { event.preventDefault(); clearSelection(); return; } if (command === "chat.newLocal") { event.preventDefault(); event.stopPropagation(); void startNewLocalThreadFromContext({ activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, handleNewThread, }); return; } if (command === "chat.new") { event.preventDefault(); event.stopPropagation(); void startNewThreadFromContext({ activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, handleNewThread, }); return; } if (command === "preview.toggle") { event.preventDefault(); event.stopPropagation(); if (!routeThreadRef) return; if (!isPreviewSupportedInRuntime()) { toastManager.add( stackedThreadToast({ type: "info", title: "Preview is desktop-only", description: "Open T3 Code in the desktop app to use the in-app preview.", }), ); return; } dispatchPreviewAction("toggle-panel"); return; } // The remaining preview commands only fire when the panel is the // currently-focused tenant. The `when: previewFocus` rule already // gates this, but defend against the keybinding being misconfigured. if ( command === "preview.refresh" || command === "preview.focusUrl" || command === "preview.zoomIn" || command === "preview.zoomOut" || command === "preview.resetZoom" ) { event.preventDefault(); event.stopPropagation(); const action = command === "preview.refresh" ? "refresh" : command === "preview.focusUrl" ? "focus-url" : command === "preview.zoomIn" ? "zoom-in" : command === "preview.zoomOut" ? "zoom-out" : "reset-zoom"; dispatchPreviewAction(action); } }; window.addEventListener("keydown", onWindowKeyDown); return () => { window.removeEventListener("keydown", onWindowKeyDown); }; }, [ activeDraftThread, activeThread, clearSelection, handleNewThread, keybindings, defaultProjectRef, previewOpen, routeThreadRef, selectedThreadKeysSize, terminalOpen, ]); return null; } function ChatRouteLayout() { return ( <> ); } export const Route = createFileRoute("/_chat")({ beforeLoad: async ({ context }) => { if ( context.authGateState.status !== "authenticated" && context.authGateState.status !== "hosted-static" ) { throw redirect({ to: "/pair", replace: true }); } }, component: ChatRouteLayout, });