import { type ResolvedKeybindingsConfig } from "@t3tools/contracts";import { useQuery } from "@tanstack/react-query";import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";import { useEffect } from "react"; import ThreadSidebar from "../components/Sidebar";import { useHandleNewThread } from "../hooks/useHandleNewThread";import { isTerminalFocused } from "../lib/terminalFocus";import { serverConfigQueryOptions } from "../lib/serverReactQuery";import { resolveShortcutCommand } from "../keybindings";import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";import { useThreadSelectionStore } from "../threadSelectionStore";import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic";import { useAppSettings } from "~/appSettings";import { Sidebar, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = [];const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16; function ChatRouteGlobalShortcuts() { const clearSelection = useThreadSelectionStore((state) => state.clearSelection); const selectedThreadIdsSize = useThreadSelectionStore((state) => state.selectedThreadIds.size); const { activeDraftThread, activeThread, handleNewThread, projects, routeThreadId } = useHandleNewThread(); const serverConfigQuery = useQuery(serverConfigQueryOptions()); const keybindings = serverConfigQuery.data?.keybindings ?? EMPTY_KEYBINDINGS; const terminalOpen = useTerminalStateStore((state) => routeThreadId ? selectThreadTerminalState(state.terminalStateByThreadId, routeThreadId).terminalOpen : false, ); const { settings: appSettings } = useAppSettings(); useEffect(() => { const onWindowKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; if (event.key === "Escape" && selectedThreadIdsSize > 0) { event.preventDefault(); clearSelection(); return; } const projectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? projects[0]?.id; if (!projectId) return; const command = resolveShortcutCommand(event, keybindings, { context: { terminalFocus: isTerminalFocused(), terminalOpen, }, }); if (command === "chat.newLocal") { event.preventDefault(); event.stopPropagation(); void handleNewThread(projectId, { envMode: resolveSidebarNewThreadEnvMode({ defaultEnvMode: appSettings.defaultThreadEnvMode, }), }); return; } if (command !== "chat.new") return; event.preventDefault(); event.stopPropagation(); void handleNewThread(projectId, { branch: activeThread?.branch ?? activeDraftThread?.branch ?? null, worktreePath: activeThread?.worktreePath ?? activeDraftThread?.worktreePath ?? null, envMode: activeDraftThread?.envMode ?? (activeThread?.worktreePath ? "worktree" : "local"), }); }; window.addEventListener("keydown", onWindowKeyDown); return () => { window.removeEventListener("keydown", onWindowKeyDown); }; }, [ activeDraftThread, activeThread, clearSelection, handleNewThread, keybindings, projects, selectedThreadIdsSize, terminalOpen, appSettings.defaultThreadEnvMode, ]); return null;} function ChatRouteLayout() { const navigate = useNavigate(); useEffect(() => { const onMenuAction = window.desktopBridge?.onMenuAction; if (typeof onMenuAction !== "function") { return; } const unsubscribe = onMenuAction((action) => { if (action !== "open-settings") return; void navigate({ to: "/settings" }); }); return () => { unsubscribe?.(); }; }, [navigate]); return ( <SidebarProvider defaultOpen> <ChatRouteGlobalShortcuts /> <Sidebar side="left" collapsible="offcanvas" className="border-r border-border bg-card text-foreground" resizable={{ minWidth: THREAD_SIDEBAR_MIN_WIDTH, shouldAcceptWidth: ({ nextWidth, wrapper }) => wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH, storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY, }} > <ThreadSidebar /> <SidebarRail /> </Sidebar> <Outlet /> </SidebarProvider> );} export const Route = createFileRoute("/_chat")({ component: ChatRouteLayout,});
import { type ResolvedKeybindingsConfig } from "@t3tools/contracts";
import { useQuery } from "@tanstack/react-query";
import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import ThreadSidebar from "../components/Sidebar";
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { isTerminalFocused } from "../lib/terminalFocus";
import { serverConfigQueryOptions } from "../lib/serverReactQuery";
import { resolveShortcutCommand } from "../keybindings";
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
import { useThreadSelectionStore } from "../threadSelectionStore";
import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic";
import { useAppSettings } from "~/appSettings";
import { Sidebar, SidebarProvider, SidebarRail } from "~/components/ui/sidebar";
const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = [];
const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;
function ChatRouteGlobalShortcuts() {
const clearSelection = useThreadSelectionStore((state) => state.clearSelection);
const selectedThreadIdsSize = useThreadSelectionStore((state) => state.selectedThreadIds.size);
const { activeDraftThread, activeThread, handleNewThread, projects, routeThreadId } =
useHandleNewThread();
const serverConfigQuery = useQuery(serverConfigQueryOptions());
const keybindings = serverConfigQuery.data?.keybindings ?? EMPTY_KEYBINDINGS;
const terminalOpen = useTerminalStateStore((state) =>
routeThreadId
? selectThreadTerminalState(state.terminalStateByThreadId, routeThreadId).terminalOpen
: false,
);
const { settings: appSettings } = useAppSettings();
useEffect(() => {
const onWindowKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) return;
if (event.key === "Escape" && selectedThreadIdsSize > 0) {
event.preventDefault();
clearSelection();
return;
}
const projectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? projects[0]?.id;
if (!projectId) return;
const command = resolveShortcutCommand(event, keybindings, {
context: {
terminalFocus: isTerminalFocused(),
terminalOpen,
},
});
if (command === "chat.newLocal") {
event.preventDefault();
event.stopPropagation();
void handleNewThread(projectId, {
envMode: resolveSidebarNewThreadEnvMode({
defaultEnvMode: appSettings.defaultThreadEnvMode,
}),
});
return;
}
if (command !== "chat.new") return;
event.preventDefault();
event.stopPropagation();
void handleNewThread(projectId, {
branch: activeThread?.branch ?? activeDraftThread?.branch ?? null,
worktreePath: activeThread?.worktreePath ?? activeDraftThread?.worktreePath ?? null,
envMode: activeDraftThread?.envMode ?? (activeThread?.worktreePath ? "worktree" : "local"),
});
};
window.addEventListener("keydown", onWindowKeyDown);
return () => {
window.removeEventListener("keydown", onWindowKeyDown);
};
}, [
activeDraftThread,
activeThread,
clearSelection,
handleNewThread,
keybindings,
projects,
selectedThreadIdsSize,
terminalOpen,
appSettings.defaultThreadEnvMode,
]);
return null;
}
function ChatRouteLayout() {
const navigate = useNavigate();
useEffect(() => {
const onMenuAction = window.desktopBridge?.onMenuAction;
if (typeof onMenuAction !== "function") {
return;
}
const unsubscribe = onMenuAction((action) => {
if (action !== "open-settings") return;
void navigate({ to: "/settings" });
});
return () => {
unsubscribe?.();
};
}, [navigate]);
return (
<SidebarProvider defaultOpen>
<ChatRouteGlobalShortcuts />
<Sidebar
side="left"
collapsible="offcanvas"
className="border-r border-border bg-card text-foreground"
resizable={{
minWidth: THREAD_SIDEBAR_MIN_WIDTH,
shouldAcceptWidth: ({ nextWidth, wrapper }) =>
wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH,
storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY,
}}
>
<ThreadSidebar />
<SidebarRail />
</Sidebar>
<Outlet />
</SidebarProvider>
);
}
export const Route = createFileRoute("/_chat")({
component: ChatRouteLayout,
});