Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ import {
normalizeModelSlug,
resolveModelSlugForProvider,
} 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";
Expand Down Expand Up @@ -85,6 +93,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";
Expand Down Expand Up @@ -169,6 +182,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`;
Expand Down Expand Up @@ -216,6 +230,28 @@ interface ChatViewProps {
threadId: ThreadId;
}

function PlanSidebarSheet(props: { children: ReactNode; open: boolean; onClose: () => void }) {
return (
<Sheet
open={props.open}
onOpenChange={(open) => {
if (!open) {
props.onClose();
}
}}
>
<SheetPopup
side="right"
showCloseButton={false}
keepMounted
className={RIGHT_PANEL_SHEET_CLASS_NAME}
>
{props.children}
</SheetPopup>
</Sheet>
);
}

export default function ChatView({ threadId }: ChatViewProps) {
const threads = useStore((store) => store.threads);
const projects = useStore((store) => store.projects);
Expand Down Expand Up @@ -316,6 +352,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
useState<Record<string, number>>({});
const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({});
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<string | null>(null);
Expand Down Expand Up @@ -1563,6 +1600,13 @@ export default function ChatView({ threadId }: ChatViewProps) {
return !open;
});
}, [activePlan?.turnId, activeProposedPlan?.turnId]);
const closePlanSidebar = useCallback(() => {
setPlanSidebarOpen(false);
const turnKey = activePlan?.turnId ?? activeProposedPlan?.turnId ?? null;
if (turnKey) {
planSidebarDismissedForTurnRef.current = turnKey;
}
}, [activePlan?.turnId, activeProposedPlan?.turnId]);

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

{/* Plan sidebar */}
{planSidebarOpen ? (
{planSidebarOpen && !shouldUsePlanSidebarSheet ? (
<PlanSidebar
activePlan={activePlan}
activeProposedPlan={activeProposedPlan}
markdownCwd={gitCwd ?? undefined}
workspaceRoot={activeProject?.cwd ?? undefined}
timestampFormat={timestampFormat}
onClose={() => {
setPlanSidebarOpen(false);
// Track that the user explicitly dismissed for this turn so auto-open won't fight them.
const turnKey = activePlan?.turnId ?? activeProposedPlan?.turnId ?? null;
if (turnKey) {
planSidebarDismissedForTurnRef.current = turnKey;
}
}}
mode="sidebar"
onClose={closePlanSidebar}
/>
) : null}
</div>
{/* end horizontal flex container */}

{shouldUsePlanSidebarSheet && planSidebarOpen ? (
<PlanSidebarSheet open onClose={closePlanSidebar}>
<PlanSidebar
activePlan={activePlan}
activeProposedPlan={activeProposedPlan}
markdownCwd={gitCwd ?? undefined}
workspaceRoot={activeProject?.cwd ?? undefined}
timestampFormat={timestampFormat}
mode="sheet"
onClose={closePlanSidebar}
/>
</PlanSidebarSheet>
) : null}

{(() => {
if (!terminalState.terminalOpen || !activeProject) {
return null;
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/components/PlanSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ interface PlanSidebarProps {
markdownCwd: string | undefined;
workspaceRoot: string | undefined;
timestampFormat: TimestampFormat;
mode?: "sheet" | "sidebar";
onClose: () => void;
}

Expand All @@ -65,6 +66,7 @@ const PlanSidebar = memo(function PlanSidebar({
markdownCwd,
workspaceRoot,
timestampFormat,
mode = "sidebar",
onClose,
}: PlanSidebarProps) {
const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false);
Expand Down Expand Up @@ -118,7 +120,14 @@ const PlanSidebar = memo(function PlanSidebar({
}, [planMarkdown, workspaceRoot]);

return (
<div className="flex h-full w-[340px] shrink-0 flex-col border-l border-border/70 bg-card/50">
<div
className={cn(
"flex min-h-0 flex-col bg-card/50",
mode === "sidebar"
? "h-full w-[340px] shrink-0 border-l border-border/70"
: "h-full w-full",
)}
>
{/* Header */}
<div className="flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3">
<div className="flex items-center gap-2">
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/rightPanelLayout.ts
Original file line number Diff line number Diff line change
@@ -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";
9 changes: 6 additions & 3 deletions apps/web/src/routes/_chat.$threadId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}
</SheetPopup>
Expand Down Expand Up @@ -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);
Expand Down