import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";import { useAtomValue } from "@effect/atom-react";import { useEffect, useMemo } from "react"; import { isCommandPaletteOpen } from "../commandPaletteBus";import { useClientSettings, useLegacySidebarEnabled } from "../hooks/useSettings";import { openCommandPalette } from "../commandPaletteBus";import { useProjects } from "../state/entities";import { usePrimaryEnvironmentId } from "../state/environments";import { selectProjectGroupingSettings } from "../logicalProject";import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";import { dispatchPreviewAction } from "../components/preview/previewActionBus";import { useHandleNewThread } from "../hooks/useHandleNewThread";import { 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 legacySidebarEnabled = useLegacySidebarEnabled(); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const projects = useProjects(); const primaryEnvironmentId = usePrimaryEnvironmentId(); const projectGroupCount = useMemo( () => buildSidebarProjectSnapshots({ projects, settings: projectGroupingSettings, primaryEnvironmentId, resolveEnvironmentLabel: () => null, }).length, [primaryEnvironmentId, projectGroupingSettings, projects], ); 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 startNewThreadFromContext({ activeDraftThread, activeThread: activeThread ?? undefined, defaultProjectRef, handleNewThread, }); return; } if (command === "chat.new") { event.preventDefault(); event.stopPropagation(); // The default sidebar routes creation through the command palette // whenever there is a real choice to make; the legacy sidebar (and // single-project setups) keep the immediate contextual create. if (!legacySidebarEnabled && projectGroupCount > 1) { openCommandPalette({ open: "new-thread-in" }); return; } 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, projectGroupCount, routeThreadRef, selectedThreadKeysSize, legacySidebarEnabled, terminalOpen, ]); return null;} function ChatRouteLayout() { return ( <> <ChatRouteGlobalShortcuts /> <Outlet /> </> );} 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,});
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { useAtomValue } from "@effect/atom-react";
import { useEffect, useMemo } from "react";
import { isCommandPaletteOpen } from "../commandPaletteBus";
import { useClientSettings, useLegacySidebarEnabled } from "../hooks/useSettings";
import { openCommandPalette } from "../commandPaletteBus";
import { useProjects } from "../state/entities";
import { usePrimaryEnvironmentId } from "../state/environments";
import { selectProjectGroupingSettings } from "../logicalProject";
import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";
import { dispatchPreviewAction } from "../components/preview/previewActionBus";
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { 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 legacySidebarEnabled = useLegacySidebarEnabled();
const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
const projects = useProjects();
const primaryEnvironmentId = usePrimaryEnvironmentId();
const projectGroupCount = useMemo(
() =>
buildSidebarProjectSnapshots({
projects,
settings: projectGroupingSettings,
primaryEnvironmentId,
resolveEnvironmentLabel: () => null,
}).length,
[primaryEnvironmentId, projectGroupingSettings, projects],
);
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 startNewThreadFromContext({
activeDraftThread,
activeThread: activeThread ?? undefined,
defaultProjectRef,
handleNewThread,
});
return;
}
if (command === "chat.new") {
event.preventDefault();
event.stopPropagation();
// The default sidebar routes creation through the command palette
// whenever there is a real choice to make; the legacy sidebar (and
// single-project setups) keep the immediate contextual create.
if (!legacySidebarEnabled && projectGroupCount > 1) {
openCommandPalette({ open: "new-thread-in" });
return;
}
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,
projectGroupCount,
routeThreadRef,
selectedThreadKeysSize,
legacySidebarEnabled,
terminalOpen,
]);
return null;
}
function ChatRouteLayout() {
return (
<>
<ChatRouteGlobalShortcuts />
<Outlet />
</>
);
}
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,
});