Skip to content

Commit b364790

Browse files
Use sheet layout for plan sidebar on narrow chat screens
- render Plan sidebar in a right-side sheet under 1180px - reuse shared right-panel layout constants for both plan and diff panels - add sidebar/sheet mode support to PlanSidebar styling and close handling
1 parent b500a36 commit b364790

4 files changed

Lines changed: 80 additions & 14 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,15 @@ import {
2828
normalizeModelSlug,
2929
resolveModelSlugForProvider,
3030
} from "@t3tools/shared/model";
31-
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
31+
import {
32+
useCallback,
33+
useEffect,
34+
useLayoutEffect,
35+
useMemo,
36+
useRef,
37+
useState,
38+
type ReactNode,
39+
} from "react";
3240
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3341
import { useDebouncedValue } from "@tanstack/react-pacer";
3442
import { useNavigate, useSearch } from "@tanstack/react-router";
@@ -85,6 +93,11 @@ import {
8593
import { basenameOfPath } from "../vscode-icons";
8694
import { useTheme } from "../hooks/useTheme";
8795
import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
96+
import { useMediaQuery } from "../hooks/useMediaQuery";
97+
import {
98+
RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY,
99+
RIGHT_PANEL_SHEET_CLASS_NAME,
100+
} from "../rightPanelLayout";
88101
import BranchToolbar from "./BranchToolbar";
89102
import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
90103
import PlanSidebar from "./PlanSidebar";
@@ -169,6 +182,7 @@ import {
169182
SendPhase,
170183
} from "./ChatView.logic";
171184
import { useLocalStorage } from "~/hooks/useLocalStorage";
185+
import { Sheet, SheetPopup } from "./ui/sheet";
172186

173187
const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000;
174188
const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;
@@ -216,6 +230,28 @@ interface ChatViewProps {
216230
threadId: ThreadId;
217231
}
218232

233+
function PlanSidebarSheet(props: { children: ReactNode; open: boolean; onClose: () => void }) {
234+
return (
235+
<Sheet
236+
open={props.open}
237+
onOpenChange={(open) => {
238+
if (!open) {
239+
props.onClose();
240+
}
241+
}}
242+
>
243+
<SheetPopup
244+
side="right"
245+
showCloseButton={false}
246+
keepMounted
247+
className={RIGHT_PANEL_SHEET_CLASS_NAME}
248+
>
249+
{props.children}
250+
</SheetPopup>
251+
</Sheet>
252+
);
253+
}
254+
219255
export default function ChatView({ threadId }: ChatViewProps) {
220256
const threads = useStore((store) => store.threads);
221257
const projects = useStore((store) => store.projects);
@@ -316,6 +352,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
316352
useState<Record<string, number>>({});
317353
const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({});
318354
const [planSidebarOpen, setPlanSidebarOpen] = useState(false);
355+
const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);
319356
const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false);
320357
// Tracks whether the user explicitly dismissed the sidebar for the active turn.
321358
const planSidebarDismissedForTurnRef = useRef<string | null>(null);
@@ -1563,6 +1600,13 @@ export default function ChatView({ threadId }: ChatViewProps) {
15631600
return !open;
15641601
});
15651602
}, [activePlan?.turnId, activeProposedPlan?.turnId]);
1603+
const closePlanSidebar = useCallback(() => {
1604+
setPlanSidebarOpen(false);
1605+
const turnKey = activePlan?.turnId ?? activeProposedPlan?.turnId ?? null;
1606+
if (turnKey) {
1607+
planSidebarDismissedForTurnRef.current = turnKey;
1608+
}
1609+
}, [activePlan?.turnId, activeProposedPlan?.turnId]);
15661610

15671611
const persistThreadSettingsForNextTurn = useCallback(
15681612
async (input: {
@@ -4016,26 +4060,34 @@ export default function ChatView({ threadId }: ChatViewProps) {
40164060
{/* end chat column */}
40174061

40184062
{/* Plan sidebar */}
4019-
{planSidebarOpen ? (
4063+
{planSidebarOpen && !shouldUsePlanSidebarSheet ? (
40204064
<PlanSidebar
40214065
activePlan={activePlan}
40224066
activeProposedPlan={activeProposedPlan}
40234067
markdownCwd={gitCwd ?? undefined}
40244068
workspaceRoot={activeProject?.cwd ?? undefined}
40254069
timestampFormat={timestampFormat}
4026-
onClose={() => {
4027-
setPlanSidebarOpen(false);
4028-
// Track that the user explicitly dismissed for this turn so auto-open won't fight them.
4029-
const turnKey = activePlan?.turnId ?? activeProposedPlan?.turnId ?? null;
4030-
if (turnKey) {
4031-
planSidebarDismissedForTurnRef.current = turnKey;
4032-
}
4033-
}}
4070+
mode="sidebar"
4071+
onClose={closePlanSidebar}
40344072
/>
40354073
) : null}
40364074
</div>
40374075
{/* end horizontal flex container */}
40384076

4077+
{shouldUsePlanSidebarSheet && planSidebarOpen ? (
4078+
<PlanSidebarSheet open onClose={closePlanSidebar}>
4079+
<PlanSidebar
4080+
activePlan={activePlan}
4081+
activeProposedPlan={activeProposedPlan}
4082+
markdownCwd={gitCwd ?? undefined}
4083+
workspaceRoot={activeProject?.cwd ?? undefined}
4084+
timestampFormat={timestampFormat}
4085+
mode="sheet"
4086+
onClose={closePlanSidebar}
4087+
/>
4088+
</PlanSidebarSheet>
4089+
) : null}
4090+
40394091
{(() => {
40404092
if (!terminalState.terminalOpen || !activeProject) {
40414093
return null;

apps/web/src/components/PlanSidebar.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ interface PlanSidebarProps {
5656
markdownCwd: string | undefined;
5757
workspaceRoot: string | undefined;
5858
timestampFormat: TimestampFormat;
59+
mode?: "sheet" | "sidebar";
5960
onClose: () => void;
6061
}
6162

@@ -65,6 +66,7 @@ const PlanSidebar = memo(function PlanSidebar({
6566
markdownCwd,
6667
workspaceRoot,
6768
timestampFormat,
69+
mode = "sidebar",
6870
onClose,
6971
}: PlanSidebarProps) {
7072
const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false);
@@ -118,7 +120,14 @@ const PlanSidebar = memo(function PlanSidebar({
118120
}, [planMarkdown, workspaceRoot]);
119121

120122
return (
121-
<div className="flex h-full w-[340px] shrink-0 flex-col border-l border-border/70 bg-card/50">
123+
<div
124+
className={cn(
125+
"flex min-h-0 flex-col bg-card/50",
126+
mode === "sidebar"
127+
? "h-full w-[340px] shrink-0 border-l border-border/70"
128+
: "h-full w-full",
129+
)}
130+
>
122131
{/* Header */}
123132
<div className="flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3">
124133
<div className="flex items-center gap-2">

apps/web/src/rightPanelLayout.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)";
2+
export const RIGHT_PANEL_SHEET_CLASS_NAME = "w-[min(88vw,820px)] max-w-[820px] p-0";

apps/web/src/routes/_chat.$threadId.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ import {
1717
stripDiffSearchParams,
1818
} from "../diffRouteSearch";
1919
import { useMediaQuery } from "../hooks/useMediaQuery";
20+
import {
21+
RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY,
22+
RIGHT_PANEL_SHEET_CLASS_NAME,
23+
} from "../rightPanelLayout";
2024
import { useStore } from "../store";
2125
import { Sheet, SheetPopup } from "../components/ui/sheet";
2226
import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar";
2327

2428
const DiffPanel = lazy(() => import("../components/DiffPanel"));
25-
const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)";
2629
const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width";
2730
const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)";
2831
const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16;
@@ -46,7 +49,7 @@ const DiffPanelSheet = (props: {
4649
side="right"
4750
showCloseButton={false}
4851
keepMounted
49-
className="w-[min(88vw,820px)] max-w-[820px] p-0"
52+
className={RIGHT_PANEL_SHEET_CLASS_NAME}
5053
>
5154
{props.children}
5255
</SheetPopup>
@@ -173,7 +176,7 @@ function ChatThreadRouteView() {
173176
);
174177
const routeThreadExists = threadExists || draftThreadExists;
175178
const diffOpen = search.diff === "1";
176-
const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);
179+
const shouldUseDiffSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);
177180
// TanStack Router keeps active route components mounted across param-only navigations
178181
// unless remountDeps are configured, so this stays warm across thread switches.
179182
const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen);

0 commit comments

Comments
 (0)