diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index eaead424fb..d06ba60b71 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -34,7 +34,15 @@ import { resolveModelSlugForProvider, supportsClaudeUltrathinkKeyword, } from "@t3tools/shared/model"; -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useDebouncedValue } from "@tanstack/react-pacer"; import { useNavigate, useSearch } from "@tanstack/react-router"; @@ -93,6 +101,11 @@ import { import { basenameOfPath } from "../vscode-icons"; import { useTheme } from "../hooks/useTheme"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; +import { useMediaQuery } from "../hooks/useMediaQuery"; +import { + RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY, + RIGHT_PANEL_SHEET_CLASS_NAME, +} from "../rightPanelLayout"; import BranchToolbar from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; @@ -178,6 +191,7 @@ import { SendPhase, } from "./ChatView.logic"; import { useLocalStorage } from "~/hooks/useLocalStorage"; +import { Sheet, SheetPopup } from "./ui/sheet"; const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000; const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; @@ -236,6 +250,28 @@ interface ChatViewProps { threadId: ThreadId; } +function PlanSidebarSheet(props: { children: ReactNode; open: boolean; onClose: () => void }) { + return ( + { + if (!open) { + props.onClose(); + } + }} + > + + {props.children} + + + ); +} + export default function ChatView({ threadId }: ChatViewProps) { const threads = useStore((store) => store.threads); const projects = useStore((store) => store.projects); @@ -334,6 +370,7 @@ export default function ChatView({ threadId }: ChatViewProps) { useState>({}); const [expandedWorkGroups, setExpandedWorkGroups] = useState>({}); const [planSidebarOpen, setPlanSidebarOpen] = useState(false); + const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); // Tracks whether the user explicitly dismissed the sidebar for the active turn. const planSidebarDismissedForTurnRef = useRef(null); @@ -1630,6 +1667,13 @@ export default function ChatView({ threadId }: ChatViewProps) { return !open; }); }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); + const closePlanSidebar = useCallback(() => { + setPlanSidebarOpen(false); + const turnKey = activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? null; + if (turnKey) { + planSidebarDismissedForTurnRef.current = turnKey; + } + }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); const persistThreadSettingsForNextTurn = useCallback( async (input: { @@ -4136,26 +4180,34 @@ export default function ChatView({ threadId }: ChatViewProps) { {/* end chat column */} {/* Plan sidebar */} - {planSidebarOpen ? ( + {planSidebarOpen && !shouldUsePlanSidebarSheet ? ( { - setPlanSidebarOpen(false); - // Track that the user explicitly dismissed for this turn so auto-open won't fight them. - const turnKey = activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? null; - if (turnKey) { - planSidebarDismissedForTurnRef.current = turnKey; - } - }} + mode="sidebar" + onClose={closePlanSidebar} /> ) : null} {/* end horizontal flex container */} + {shouldUsePlanSidebarSheet && planSidebarOpen ? ( + + + + ) : null} + {(() => { if (!terminalState.terminalOpen || !activeProject) { return null; diff --git a/apps/web/src/components/PlanSidebar.tsx b/apps/web/src/components/PlanSidebar.tsx index 47bee930cc..4c3fa67704 100644 --- a/apps/web/src/components/PlanSidebar.tsx +++ b/apps/web/src/components/PlanSidebar.tsx @@ -56,6 +56,7 @@ interface PlanSidebarProps { markdownCwd: string | undefined; workspaceRoot: string | undefined; timestampFormat: TimestampFormat; + mode?: "sheet" | "sidebar"; onClose: () => void; } @@ -65,6 +66,7 @@ const PlanSidebar = memo(function PlanSidebar({ markdownCwd, workspaceRoot, timestampFormat, + mode = "sidebar", onClose, }: PlanSidebarProps) { const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false); @@ -118,7 +120,14 @@ const PlanSidebar = memo(function PlanSidebar({ }, [planMarkdown, workspaceRoot]); return ( -
+
{/* Header */}
diff --git a/apps/web/src/rightPanelLayout.ts b/apps/web/src/rightPanelLayout.ts new file mode 100644 index 0000000000..c94f52a9cb --- /dev/null +++ b/apps/web/src/rightPanelLayout.ts @@ -0,0 +1,2 @@ +export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; +export const RIGHT_PANEL_SHEET_CLASS_NAME = "w-[min(88vw,820px)] max-w-[820px] p-0"; diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx index 8e7a5d3ba8..741c74f397 100644 --- a/apps/web/src/routes/_chat.$threadId.tsx +++ b/apps/web/src/routes/_chat.$threadId.tsx @@ -17,12 +17,15 @@ import { stripDiffSearchParams, } from "../diffRouteSearch"; import { useMediaQuery } from "../hooks/useMediaQuery"; +import { + RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY, + RIGHT_PANEL_SHEET_CLASS_NAME, +} from "../rightPanelLayout"; import { useStore } from "../store"; import { Sheet, SheetPopup } from "../components/ui/sheet"; import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; const DiffPanel = lazy(() => import("../components/DiffPanel")); -const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width"; const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; @@ -46,7 +49,7 @@ const DiffPanelSheet = (props: { side="right" showCloseButton={false} keepMounted - className="w-[min(88vw,820px)] max-w-[820px] p-0" + className={RIGHT_PANEL_SHEET_CLASS_NAME} > {props.children} @@ -173,7 +176,7 @@ function ChatThreadRouteView() { ); const routeThreadExists = threadExists || draftThreadExists; const diffOpen = search.diff === "1"; - const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); + const shouldUseDiffSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); // TanStack Router keeps active route components mounted across param-only navigations // unless remountDeps are configured, so this stays warm across thread switches. const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen);