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 (
<>
>
);
}
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,
});