| author | |
| committer | |
| log | 19d47408a53181446a02fac967846cc15d77d5b8 |
| tree | bbc6282a756d0794789a4b918d512ab71636a04a |
| parent | 3e07f5a6a412be551e1d74fe4600a31e5ef557c4 |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 75 insertions(+), 40 deletions(-)
apps/web/src/components/ChatView.tsx+27-7| ... | ... | @@ -92,6 +92,8 @@ import { useTheme } from "../hooks/useTheme"; |
| 92 | 92 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; |
| 93 | 93 | import { useCommandPaletteStore } from "../commandPaletteStore"; |
| 94 | 94 | import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; |
| 95 | import { useMediaQuery } from "../hooks/useMediaQuery"; | |
| 96 | import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; | |
| 95 | 97 | import { BranchToolbar } from "./BranchToolbar"; |
| 96 | 98 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; |
| 97 | 99 | import PlanSidebar from "./PlanSidebar"; |
| ... | ... | @@ -171,6 +173,7 @@ import { |
| 171 | 173 | } from "~/rpc/serverState"; |
| 172 | 174 | import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; |
| 173 | 175 | import { retainThreadDetailSubscription } from "../environments/runtime/service"; |
| 176 | import { RightPanelSheet } from "./RightPanelSheet"; | |
| 174 | 177 | |
| 175 | 178 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = |
| 176 | 179 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; |
| ... | ... | @@ -675,6 +678,7 @@ export default function ChatView(props: ChatViewProps) { |
| 675 | 678 | const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] = |
| 676 | 679 | useState<Record<string, number>>({}); |
| 677 | 680 | const [planSidebarOpen, setPlanSidebarOpen] = useState(false); |
| 681 | const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); | |
| 678 | 682 | // Tracks whether the user explicitly dismissed the sidebar for the active turn. |
| 679 | 683 | const planSidebarDismissedForTurnRef = useRef<string | null>(null); |
| 680 | 684 | // When set, the thread-change reset effect will open the sidebar instead of closing it. |
| ... | ... | @@ -1897,6 +1901,11 @@ export default function ChatView(props: ChatViewProps) { |
| 1897 | 1901 | return !open; |
| 1898 | 1902 | }); |
| 1899 | 1903 | }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); |
| 1904 | const closePlanSidebar = useCallback(() => { | |
| 1905 | setPlanSidebarOpen(false); | |
| 1906 | planSidebarDismissedForTurnRef.current = | |
| 1907 | activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; | |
| 1908 | }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); | |
| 1900 | 1909 | |
| 1901 | 1910 | const persistThreadSettingsForNextTurn = useCallback( |
| 1902 | 1911 | async (input: { |
| ... | ... | @@ -3394,7 +3403,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3394 | 3403 | {/* end chat column */} |
| 3395 | 3404 | |
| 3396 | 3405 | {/* Plan sidebar */} |
| 3397 | {planSidebarOpen ? ( | |
| 3406 | {planSidebarOpen && !shouldUsePlanSidebarSheet ? ( | |
| 3398 | 3407 | <PlanSidebar |
| 3399 | 3408 | activePlan={activePlan} |
| 3400 | 3409 | activeProposedPlan={sidebarProposedPlan} |
| ... | ... | @@ -3403,12 +3412,8 @@ export default function ChatView(props: ChatViewProps) { |
| 3403 | 3412 | markdownCwd={gitCwd ?? undefined} |
| 3404 | 3413 | workspaceRoot={activeWorkspaceRoot} |
| 3405 | 3414 | timestampFormat={timestampFormat} |
| 3406 | onClose={() => { | |
| 3407 | setPlanSidebarOpen(false); | |
| 3408 | // Track that the user explicitly dismissed for this turn so auto-open won't fight them. | |
| 3409 | planSidebarDismissedForTurnRef.current = | |
| 3410 | activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; | |
| 3411 | }} | |
| 3415 | mode="sidebar" | |
| 3416 | onClose={closePlanSidebar} | |
| 3412 | 3417 | /> |
| 3413 | 3418 | ) : null} |
| 3414 | 3419 | </div> |
| ... | ... | @@ -3430,6 +3435,21 @@ export default function ChatView(props: ChatViewProps) { |
| 3430 | 3435 | onAddTerminalContext={addTerminalContextToDraft} |
| 3431 | 3436 | /> |
| 3432 | 3437 | ))} |
| 3438 | {shouldUsePlanSidebarSheet ? ( | |
| 3439 | <RightPanelSheet open={planSidebarOpen} onClose={closePlanSidebar}> | |
| 3440 | <PlanSidebar | |
| 3441 | activePlan={activePlan} | |
| 3442 | activeProposedPlan={sidebarProposedPlan} | |
| 3443 | label={planSidebarLabel} | |
| 3444 | environmentId={environmentId} | |
| 3445 | markdownCwd={gitCwd ?? undefined} | |
| 3446 | workspaceRoot={activeWorkspaceRoot} | |
| 3447 | timestampFormat={timestampFormat} | |
| 3448 | mode="sheet" | |
| 3449 | onClose={closePlanSidebar} | |
| 3450 | /> | |
| 3451 | </RightPanelSheet> | |
| 3452 | ) : null} | |
| 3433 | 3453 | |
| 3434 | 3454 | {expandedImage && ( |
| 3435 | 3455 | <ExpandedImageDialog preview={expandedImage} onClose={closeExpandedImage} /> |
apps/web/src/components/PlanSidebar.tsx+10-1| ... | ... | @@ -59,6 +59,7 @@ interface PlanSidebarProps { |
| 59 | 59 | markdownCwd: string | undefined; |
| 60 | 60 | workspaceRoot: string | undefined; |
| 61 | 61 | timestampFormat: TimestampFormat; |
| 62 | mode?: "sheet" | "sidebar"; | |
| 62 | 63 | onClose: () => void; |
| 63 | 64 | } |
| 64 | 65 | |
| ... | ... | @@ -70,6 +71,7 @@ const PlanSidebar = memo(function PlanSidebar({ |
| 70 | 71 | markdownCwd, |
| 71 | 72 | workspaceRoot, |
| 72 | 73 | timestampFormat, |
| 74 | mode = "sidebar", | |
| 73 | 75 | onClose, |
| 74 | 76 | }: PlanSidebarProps) { |
| 75 | 77 | const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false); |
| ... | ... | @@ -123,7 +125,14 @@ const PlanSidebar = memo(function PlanSidebar({ |
| 123 | 125 | }, [environmentId, planMarkdown, workspaceRoot]); |
| 124 | 126 | |
| 125 | 127 | return ( |
| 126 | <div className="flex h-full w-[340px] shrink-0 flex-col border-l border-border/70 bg-card/50"> | |
| 128 | <div | |
| 129 | className={cn( | |
| 130 | "flex min-h-0 flex-col bg-card/50", | |
| 131 | mode === "sidebar" | |
| 132 | ? "h-full w-[340px] shrink-0 border-l border-border/70" | |
| 133 | : "h-full w-full", | |
| 134 | )} | |
| 135 | > | |
| 127 | 136 | {/* Header */} |
| 128 | 137 | <div className="flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3"> |
| 129 | 138 | <div className="flex items-center gap-2"> |
apps/web/src/components/RightPanelSheet.tsx created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | import { type ReactNode } from "react"; | |
| 2 | ||
| 3 | import { RIGHT_PANEL_SHEET_CLASS_NAME } from "../rightPanelLayout"; | |
| 4 | import { Sheet, SheetPopup } from "./ui/sheet"; | |
| 5 | ||
| 6 | export function RightPanelSheet(props: { | |
| 7 | children: ReactNode; | |
| 8 | open: boolean; | |
| 9 | onClose: () => void; | |
| 10 | }) { | |
| 11 | return ( | |
| 12 | <Sheet | |
| 13 | open={props.open} | |
| 14 | onOpenChange={(open) => { | |
| 15 | if (!open) { | |
| 16 | props.onClose(); | |
| 17 | } | |
| 18 | }} | |
| 19 | > | |
| 20 | <SheetPopup | |
| 21 | side="right" | |
| 22 | showCloseButton={false} | |
| 23 | keepMounted | |
| 24 | className={RIGHT_PANEL_SHEET_CLASS_NAME} | |
| 25 | > | |
| 26 | {props.children} | |
| 27 | </SheetPopup> | |
| 28 | </Sheet> | |
| 29 | ); | |
| 30 | } |
apps/web/src/rightPanelLayout.ts created+2| ... | ... | @@ -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.$environmentId.$threadId.tsx+6-32| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; |
| 2 | import { Suspense, lazy, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; | |
| 2 | import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from "react"; | |
| 3 | 3 | |
| 4 | 4 | import ChatView from "../components/ChatView"; |
| 5 | 5 | import { threadHasStarted } from "../components/ChatView.logic"; |
| ... | ... | @@ -17,45 +17,19 @@ import { |
| 17 | 17 | stripDiffSearchParams, |
| 18 | 18 | } from "../diffRouteSearch"; |
| 19 | 19 | import { useMediaQuery } from "../hooks/useMediaQuery"; |
| 20 | import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; | |
| 20 | 21 | import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../store"; |
| 21 | 22 | import { createThreadSelectorByRef } from "../storeSelectors"; |
| 22 | 23 | import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes"; |
| 23 | import { Sheet, SheetPopup } from "../components/ui/sheet"; | |
| 24 | import { RightPanelSheet } from "../components/RightPanelSheet"; | |
| 24 | 25 | import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; |
| 25 | 26 | |
| 26 | 27 | const DiffPanel = lazy(() => import("../components/DiffPanel")); |
| 27 | const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; | |
| 28 | 28 | const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width"; |
| 29 | 29 | const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; |
| 30 | 30 | const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; |
| 31 | 31 | const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208; |
| 32 | 32 | |
| 33 | const DiffPanelSheet = (props: { | |
| 34 | children: ReactNode; | |
| 35 | diffOpen: boolean; | |
| 36 | onCloseDiff: () => void; | |
| 37 | }) => { | |
| 38 | return ( | |
| 39 | <Sheet | |
| 40 | open={props.diffOpen} | |
| 41 | onOpenChange={(open) => { | |
| 42 | if (!open) { | |
| 43 | props.onCloseDiff(); | |
| 44 | } | |
| 45 | }} | |
| 46 | > | |
| 47 | <SheetPopup | |
| 48 | side="right" | |
| 49 | showCloseButton={false} | |
| 50 | keepMounted | |
| 51 | className="w-[min(88vw,820px)] max-w-[820px] p-0" | |
| 52 | > | |
| 53 | {props.children} | |
| 54 | </SheetPopup> | |
| 55 | </Sheet> | |
| 56 | ); | |
| 57 | }; | |
| 58 | ||
| 59 | 33 | const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { |
| 60 | 34 | return ( |
| 61 | 35 | <DiffPanelShell mode={props.mode} header={<DiffPanelHeaderSkeleton />}> |
| ... | ... | @@ -192,7 +166,7 @@ function ChatThreadRouteView() { |
| 192 | 166 | const serverThreadStarted = threadHasStarted(serverThread); |
| 193 | 167 | const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads; |
| 194 | 168 | const diffOpen = search.diff === "1"; |
| 195 | const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); | |
| 169 | const shouldUseDiffSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); | |
| 196 | 170 | const currentThreadKey = threadRef ? `${threadRef.environmentId}:${threadRef.threadId}` : null; |
| 197 | 171 | const [diffPanelMountState, setDiffPanelMountState] = useState(() => ({ |
| 198 | 172 | threadKey: currentThreadKey, |
| ... | ... | @@ -293,9 +267,9 @@ function ChatThreadRouteView() { |
| 293 | 267 | routeKind="server" |
| 294 | 268 | /> |
| 295 | 269 | </SidebarInset> |
| 296 | <DiffPanelSheet diffOpen={diffOpen} onCloseDiff={closeDiff}> | |
| 270 | <RightPanelSheet open={diffOpen} onClose={closeDiff}> | |
| 297 | 271 | {shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null} |
| 298 | </DiffPanelSheet> | |
| 272 | </RightPanelSheet> | |
| 299 | 273 | </> |
| 300 | 274 | ); |
| 301 | 275 | } |