| 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,6 +92,8 @@ import { useTheme } from "../hooks/useTheme"; |
| 92 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; | 92 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; |
| 93 | import { useCommandPaletteStore } from "../commandPaletteStore"; | 93 | import { useCommandPaletteStore } from "../commandPaletteStore"; |
| 94 | import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; | 94 | import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; |
| 95 | import { useMediaQuery } from "../hooks/useMediaQuery"; | ||
| 96 | import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; | ||
| 95 | import { BranchToolbar } from "./BranchToolbar"; | 97 | import { BranchToolbar } from "./BranchToolbar"; |
| 96 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; | 98 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; |
| 97 | import PlanSidebar from "./PlanSidebar"; | 99 | import PlanSidebar from "./PlanSidebar"; |
| ... | @@ -171,6 +173,7 @@ import { | ... | @@ -171,6 +173,7 @@ import { |
| 171 | } from "~/rpc/serverState"; | 173 | } from "~/rpc/serverState"; |
| 172 | import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; | 174 | import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; |
| 173 | import { retainThreadDetailSubscription } from "../environments/runtime/service"; | 175 | import { retainThreadDetailSubscription } from "../environments/runtime/service"; |
| 176 | import { RightPanelSheet } from "./RightPanelSheet"; | ||
| 174 | 177 | ||
| 175 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = | 178 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = |
| 176 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; | 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,6 +678,7 @@ export default function ChatView(props: ChatViewProps) { |
| 675 | const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] = | 678 | const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] = |
| 676 | useState<Record<string, number>>({}); | 679 | useState<Record<string, number>>({}); |
| 677 | const [planSidebarOpen, setPlanSidebarOpen] = useState(false); | 680 | const [planSidebarOpen, setPlanSidebarOpen] = useState(false); |
| 681 | const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); | ||
| 678 | // Tracks whether the user explicitly dismissed the sidebar for the active turn. | 682 | // Tracks whether the user explicitly dismissed the sidebar for the active turn. |
| 679 | const planSidebarDismissedForTurnRef = useRef<string | null>(null); | 683 | const planSidebarDismissedForTurnRef = useRef<string | null>(null); |
| 680 | // When set, the thread-change reset effect will open the sidebar instead of closing it. | 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,6 +1901,11 @@ export default function ChatView(props: ChatViewProps) { |
| 1897 | return !open; | 1901 | return !open; |
| 1898 | }); | 1902 | }); |
| 1899 | }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); | 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 | const persistThreadSettingsForNextTurn = useCallback( | 1910 | const persistThreadSettingsForNextTurn = useCallback( |
| 1902 | async (input: { | 1911 | async (input: { |
| ... | @@ -3394,7 +3403,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3394,7 +3403,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3394 | {/* end chat column */} | 3403 | {/* end chat column */} |
| 3395 | 3404 | ||
| 3396 | {/* Plan sidebar */} | 3405 | {/* Plan sidebar */} |
| 3397 | {planSidebarOpen ? ( | 3406 | {planSidebarOpen && !shouldUsePlanSidebarSheet ? ( |
| 3398 | <PlanSidebar | 3407 | <PlanSidebar |
| 3399 | activePlan={activePlan} | 3408 | activePlan={activePlan} |
| 3400 | activeProposedPlan={sidebarProposedPlan} | 3409 | activeProposedPlan={sidebarProposedPlan} |
| ... | @@ -3403,12 +3412,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3403,12 +3412,8 @@ export default function ChatView(props: ChatViewProps) { |
| 3403 | markdownCwd={gitCwd ?? undefined} | 3412 | markdownCwd={gitCwd ?? undefined} |
| 3404 | workspaceRoot={activeWorkspaceRoot} | 3413 | workspaceRoot={activeWorkspaceRoot} |
| 3405 | timestampFormat={timestampFormat} | 3414 | timestampFormat={timestampFormat} |
| 3406 | onClose={() => { | 3415 | mode="sidebar" |
| 3407 | setPlanSidebarOpen(false); | 3416 | onClose={closePlanSidebar} |
| 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 | }} | ||
| 3412 | /> | 3417 | /> |
| 3413 | ) : null} | 3418 | ) : null} |
| 3414 | </div> | 3419 | </div> |
| ... | @@ -3430,6 +3435,21 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3430,6 +3435,21 @@ export default function ChatView(props: ChatViewProps) { |
| 3430 | onAddTerminalContext={addTerminalContextToDraft} | 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 | {expandedImage && ( | 3454 | {expandedImage && ( |
| 3435 | <ExpandedImageDialog preview={expandedImage} onClose={closeExpandedImage} /> | 3455 | <ExpandedImageDialog preview={expandedImage} onClose={closeExpandedImage} /> |
apps/web/src/components/PlanSidebar.tsx+10-1| ... | @@ -59,6 +59,7 @@ interface PlanSidebarProps { | ... | @@ -59,6 +59,7 @@ interface PlanSidebarProps { |
| 59 | markdownCwd: string | undefined; | 59 | markdownCwd: string | undefined; |
| 60 | workspaceRoot: string | undefined; | 60 | workspaceRoot: string | undefined; |
| 61 | timestampFormat: TimestampFormat; | 61 | timestampFormat: TimestampFormat; |
| 62 | mode?: "sheet" | "sidebar"; | ||
| 62 | onClose: () => void; | 63 | onClose: () => void; |
| 63 | } | 64 | } |
| 64 | 65 | ||
| ... | @@ -70,6 +71,7 @@ const PlanSidebar = memo(function PlanSidebar({ | ... | @@ -70,6 +71,7 @@ const PlanSidebar = memo(function PlanSidebar({ |
| 70 | markdownCwd, | 71 | markdownCwd, |
| 71 | workspaceRoot, | 72 | workspaceRoot, |
| 72 | timestampFormat, | 73 | timestampFormat, |
| 74 | mode = "sidebar", | ||
| 73 | onClose, | 75 | onClose, |
| 74 | }: PlanSidebarProps) { | 76 | }: PlanSidebarProps) { |
| 75 | const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false); | 77 | const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false); |
| ... | @@ -123,7 +125,14 @@ const PlanSidebar = memo(function PlanSidebar({ | ... | @@ -123,7 +125,14 @@ const PlanSidebar = memo(function PlanSidebar({ |
| 123 | }, [environmentId, planMarkdown, workspaceRoot]); | 125 | }, [environmentId, planMarkdown, workspaceRoot]); |
| 124 | 126 | ||
| 125 | return ( | 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 | {/* Header */} | 136 | {/* Header */} |
| 128 | <div className="flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3"> | 137 | <div className="flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3"> |
| 129 | <div className="flex items-center gap-2"> | 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,5 +1,5 @@ |
| 1 | import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; | 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 | import ChatView from "../components/ChatView"; | 4 | import ChatView from "../components/ChatView"; |
| 5 | import { threadHasStarted } from "../components/ChatView.logic"; | 5 | import { threadHasStarted } from "../components/ChatView.logic"; |
| ... | @@ -17,45 +17,19 @@ import { | ... | @@ -17,45 +17,19 @@ import { |
| 17 | stripDiffSearchParams, | 17 | stripDiffSearchParams, |
| 18 | } from "../diffRouteSearch"; | 18 | } from "../diffRouteSearch"; |
| 19 | import { useMediaQuery } from "../hooks/useMediaQuery"; | 19 | import { useMediaQuery } from "../hooks/useMediaQuery"; |
| 20 | import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; | ||
| 20 | import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../store"; | 21 | import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../store"; |
| 21 | import { createThreadSelectorByRef } from "../storeSelectors"; | 22 | import { createThreadSelectorByRef } from "../storeSelectors"; |
| 22 | import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes"; | 23 | import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes"; |
| 23 | import { Sheet, SheetPopup } from "../components/ui/sheet"; | 24 | import { RightPanelSheet } from "../components/RightPanelSheet"; |
| 24 | import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; | 25 | import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar"; |
| 25 | 26 | ||
| 26 | const DiffPanel = lazy(() => import("../components/DiffPanel")); | 27 | const DiffPanel = lazy(() => import("../components/DiffPanel")); |
| 27 | const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)"; | ||
| 28 | const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width"; | 28 | const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width"; |
| 29 | const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; | 29 | const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)"; |
| 30 | const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; | 30 | const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16; |
| 31 | const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208; | 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 | const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { | 33 | const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { |
| 60 | return ( | 34 | return ( |
| 61 | <DiffPanelShell mode={props.mode} header={<DiffPanelHeaderSkeleton />}> | 35 | <DiffPanelShell mode={props.mode} header={<DiffPanelHeaderSkeleton />}> |
| ... | @@ -192,7 +166,7 @@ function ChatThreadRouteView() { | ... | @@ -192,7 +166,7 @@ function ChatThreadRouteView() { |
| 192 | const serverThreadStarted = threadHasStarted(serverThread); | 166 | const serverThreadStarted = threadHasStarted(serverThread); |
| 193 | const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads; | 167 | const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads; |
| 194 | const diffOpen = search.diff === "1"; | 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 | const currentThreadKey = threadRef ? `${threadRef.environmentId}:${threadRef.threadId}` : null; | 170 | const currentThreadKey = threadRef ? `${threadRef.environmentId}:${threadRef.threadId}` : null; |
| 197 | const [diffPanelMountState, setDiffPanelMountState] = useState(() => ({ | 171 | const [diffPanelMountState, setDiffPanelMountState] = useState(() => ({ |
| 198 | threadKey: currentThreadKey, | 172 | threadKey: currentThreadKey, |
| ... | @@ -293,9 +267,9 @@ function ChatThreadRouteView() { | ... | @@ -293,9 +267,9 @@ function ChatThreadRouteView() { |
| 293 | routeKind="server" | 267 | routeKind="server" |
| 294 | /> | 268 | /> |
| 295 | </SidebarInset> | 269 | </SidebarInset> |
| 296 | <DiffPanelSheet diffOpen={diffOpen} onCloseDiff={closeDiff}> | 270 | <RightPanelSheet open={diffOpen} onClose={closeDiff}> |
| 297 | {shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null} | 271 | {shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null} |
| 298 | </DiffPanelSheet> | 272 | </RightPanelSheet> |
| 299 | </> | 273 | </> |
| 300 | ); | 274 | ); |
| 301 | } | 275 | } |