From 34f04f03ded96894447c68074cc28f8a12986306 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Wed, 18 Mar 2026 23:26:01 +0530 Subject: [PATCH 1/4] 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 --- apps/web/src/components/ChatView.tsx | 70 ++++++++++++++++--- apps/web/src/components/PlanSidebar.tsx | 11 ++- apps/web/src/rightPanelLayout.ts | 2 + .../routes/_chat.$environmentId.$threadId.tsx | 9 ++- 4 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/rightPanelLayout.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index c16aab4fbe7f..ac19ab2d71ed 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -29,7 +29,15 @@ import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; import { truncate } from "@t3tools/shared/String"; import { Debouncer } from "@tanstack/react-pacer"; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { useShallow } from "zustand/react/shallow"; import { useGitStatus } from "~/lib/gitStatusState"; @@ -92,6 +100,11 @@ import { useTheme } from "../hooks/useTheme"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useCommandPaletteStore } from "../commandPaletteStore"; import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; +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"; @@ -171,6 +184,7 @@ import { } from "~/rpc/serverState"; import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; import { retainThreadDetailSubscription } from "../environments/runtime/service"; +import { Sheet, SheetPopup } from "./ui/sheet"; const IMAGE_ONLY_BOOTSTRAP_PROMPT = "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; @@ -576,6 +590,28 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra ); }); +function PlanSidebarSheet(props: { children: ReactNode; open: boolean; onClose: () => void }) { + return ( + { + if (!open) { + props.onClose(); + } + }} + > + + {props.children} + + + ); +} + export default function ChatView(props: ChatViewProps) { const { environmentId, @@ -675,6 +711,7 @@ export default function ChatView(props: ChatViewProps) { const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] = useState>({}); const [planSidebarOpen, setPlanSidebarOpen] = useState(false); + const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); // Tracks whether the user explicitly dismissed the sidebar for the active turn. const planSidebarDismissedForTurnRef = useRef(null); // When set, the thread-change reset effect will open the sidebar instead of closing it. @@ -1897,6 +1934,13 @@ export default function ChatView(props: 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: { @@ -3394,7 +3438,7 @@ export default function ChatView(props: 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. - planSidebarDismissedForTurnRef.current = - activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; - }} + mode="sidebar" + onClose={closePlanSidebar} /> ) : null} @@ -3430,6 +3470,20 @@ export default function ChatView(props: ChatViewProps) { onAddTerminalContext={addTerminalContextToDraft} /> ))} + {shouldUsePlanSidebarSheet ? ( + + + + ) : null} {expandedImage && ( diff --git a/apps/web/src/components/PlanSidebar.tsx b/apps/web/src/components/PlanSidebar.tsx index 489e38f48d98..00b9da2b0c87 100644 --- a/apps/web/src/components/PlanSidebar.tsx +++ b/apps/web/src/components/PlanSidebar.tsx @@ -59,6 +59,7 @@ interface PlanSidebarProps { markdownCwd: string | undefined; workspaceRoot: string | undefined; timestampFormat: TimestampFormat; + mode?: "sheet" | "sidebar"; onClose: () => void; } @@ -70,6 +71,7 @@ const PlanSidebar = memo(function PlanSidebar({ markdownCwd, workspaceRoot, timestampFormat, + mode = "sidebar", onClose, }: PlanSidebarProps) { const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false); @@ -123,7 +125,14 @@ const PlanSidebar = memo(function PlanSidebar({ }, [environmentId, planMarkdown, workspaceRoot]); return ( -
+
{/* Header */}
diff --git a/apps/web/src/rightPanelLayout.ts b/apps/web/src/rightPanelLayout.ts new file mode 100644 index 000000000000..c94f52a9cb21 --- /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.$environmentId.$threadId.tsx b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx index fa3f59b93f33..7f62f097d481 100644 --- a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx +++ b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx @@ -17,6 +17,10 @@ import { stripDiffSearchParams, } from "../diffRouteSearch"; import { useMediaQuery } from "../hooks/useMediaQuery"; +import { + RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY, + RIGHT_PANEL_SHEET_CLASS_NAME, +} from "../rightPanelLayout"; import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../store"; import { createThreadSelectorByRef } from "../storeSelectors"; import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes"; @@ -24,7 +28,6 @@ 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; @@ -48,7 +51,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} @@ -192,7 +195,7 @@ function ChatThreadRouteView() { const serverThreadStarted = threadHasStarted(serverThread); const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads; const diffOpen = search.diff === "1"; - const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); + const shouldUseDiffSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); const currentThreadKey = threadRef ? `${threadRef.environmentId}:${threadRef.threadId}` : null; const [diffPanelMountState, setDiffPanelMountState] = useState(() => ({ threadKey: currentThreadKey, From c798ac8410dbf37526ce1465645e08f79765e913 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Mon, 13 Apr 2026 22:30:09 +0530 Subject: [PATCH 2/4] style(web): format ChatView import after rebase --- apps/web/src/components/ChatView.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ac19ab2d71ed..2a5a9785bff5 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -29,15 +29,7 @@ import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; import { truncate } from "@t3tools/shared/String"; import { Debouncer } from "@tanstack/react-pacer"; -import { - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, - type ReactNode, -} from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { useShallow } from "zustand/react/shallow"; import { useGitStatus } from "~/lib/gitStatusState"; From 2e3a433cf8b841e882f460a69a324fdedf34f0fa Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Thu, 16 Apr 2026 21:11:18 +0530 Subject: [PATCH 3/4] Extract reusable right-panel sheet wrapper - Reuse the shared sheet shell for plan and diff sidebars - Keep responsive right-panel behavior consistent across routes --- apps/web/src/components/ChatView.tsx | 36 +++-------------- apps/web/src/components/RightPanelSheet.tsx | 30 ++++++++++++++ .../routes/_chat.$environmentId.$threadId.tsx | 39 +++---------------- 3 files changed, 41 insertions(+), 64 deletions(-) create mode 100644 apps/web/src/components/RightPanelSheet.tsx diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 2a5a9785bff5..bbf1625bcb1b 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -29,7 +29,7 @@ import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; import { truncate } from "@t3tools/shared/String"; import { Debouncer } from "@tanstack/react-pacer"; -import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { useShallow } from "zustand/react/shallow"; import { useGitStatus } from "~/lib/gitStatusState"; @@ -93,10 +93,7 @@ import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useCommandPaletteStore } from "../commandPaletteStore"; import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; import { useMediaQuery } from "../hooks/useMediaQuery"; -import { - RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY, - RIGHT_PANEL_SHEET_CLASS_NAME, -} from "../rightPanelLayout"; +import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; import { BranchToolbar } from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; @@ -176,7 +173,7 @@ import { } from "~/rpc/serverState"; import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; import { retainThreadDetailSubscription } from "../environments/runtime/service"; -import { Sheet, SheetPopup } from "./ui/sheet"; +import { RightPanelSheet } from "./RightPanelSheet"; const IMAGE_ONLY_BOOTSTRAP_PROMPT = "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; @@ -582,28 +579,6 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra ); }); -function PlanSidebarSheet(props: { children: ReactNode; open: boolean; onClose: () => void }) { - return ( - { - if (!open) { - props.onClose(); - } - }} - > - - {props.children} - - - ); -} - export default function ChatView(props: ChatViewProps) { const { environmentId, @@ -3463,10 +3438,11 @@ export default function ChatView(props: ChatViewProps) { /> ))} {shouldUsePlanSidebarSheet ? ( - + - + ) : null} {expandedImage && ( diff --git a/apps/web/src/components/RightPanelSheet.tsx b/apps/web/src/components/RightPanelSheet.tsx new file mode 100644 index 000000000000..ebc4aa0a698f --- /dev/null +++ b/apps/web/src/components/RightPanelSheet.tsx @@ -0,0 +1,30 @@ +import { type ReactNode } from "react"; + +import { RIGHT_PANEL_SHEET_CLASS_NAME } from "../rightPanelLayout"; +import { Sheet, SheetPopup } from "./ui/sheet"; + +export function RightPanelSheet(props: { + children: ReactNode; + open: boolean; + onClose: () => void; +}) { + return ( + { + if (!open) { + props.onClose(); + } + }} + > + + {props.children} + + + ); +} diff --git a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx index 7f62f097d481..ff20673e2deb 100644 --- a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx +++ b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx @@ -1,5 +1,5 @@ import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; -import { Suspense, lazy, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from "react"; import ChatView from "../components/ChatView"; import { threadHasStarted } from "../components/ChatView.logic"; @@ -17,14 +17,11 @@ import { stripDiffSearchParams, } from "../diffRouteSearch"; import { useMediaQuery } from "../hooks/useMediaQuery"; -import { - RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY, - RIGHT_PANEL_SHEET_CLASS_NAME, -} from "../rightPanelLayout"; +import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../store"; import { createThreadSelectorByRef } from "../storeSelectors"; import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes"; -import { Sheet, SheetPopup } from "../components/ui/sheet"; +import { RightPanelSheet } from "../components/RightPanelSheet"; import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; const DiffPanel = lazy(() => import("../components/DiffPanel")); @@ -33,32 +30,6 @@ const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208; -const DiffPanelSheet = (props: { - children: ReactNode; - diffOpen: boolean; - onCloseDiff: () => void; -}) => { - return ( - { - if (!open) { - props.onCloseDiff(); - } - }} - > - - {props.children} - - - ); -}; - const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { return ( }> @@ -296,9 +267,9 @@ function ChatThreadRouteView() { routeKind="server" /> - + {shouldRenderDiffContent ? : null} - + ); } From aa20d2aa33df127ca63339c6c44cd737ecb49557 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Thu, 16 Apr 2026 21:13:11 +0530 Subject: [PATCH 4/4] Persist plan sidebar dismissal state - Mark the sidebar as dismissed even when no turn ID is active - Prevent the plan sidebar from reopening unexpectedly --- apps/web/src/components/ChatView.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index bbf1625bcb1b..f1e910095fa6 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1903,10 +1903,8 @@ export default function ChatView(props: ChatViewProps) { }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); const closePlanSidebar = useCallback(() => { setPlanSidebarOpen(false); - const turnKey = activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? null; - if (turnKey) { - planSidebarDismissedForTurnRef.current = turnKey; - } + planSidebarDismissedForTurnRef.current = + activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); const persistThreadSettingsForNextTurn = useCallback(