Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
64 changes: 54 additions & 10 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
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";
Expand Down Expand Up @@ -91,6 +91,11 @@
import { useTheme } from "../hooks/useTheme";
import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
import { useCommandPaletteStore } from "../commandPaletteStore";
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 @@ -170,6 +175,7 @@
} 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).]";
Expand Down Expand Up @@ -575,6 +581,28 @@
);
});

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>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlanSidebarSheet duplicates existing DiffPanelSheet component

Low Severity

PlanSidebarSheet is structurally identical to DiffPanelSheet in the route file — both wrap children in a Sheet + SheetPopup with the same side, showCloseButton, keepMounted, and RIGHT_PANEL_SHEET_CLASS_NAME props, differing only in prop naming. Since the PR already extracted shared constants into rightPanelLayout.ts, this wrapper component is the natural next candidate for sharing, avoiding duplicated logic and ensuring future changes (e.g., adding an overlay style) only need to happen in one place.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ec1aada. Configure here.

}

export default function ChatView(props: ChatViewProps) {
const {
environmentId,
Expand Down Expand Up @@ -674,6 +702,7 @@
const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] =
useState<Record<string, number>>({});
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<string | null>(null);
// When set, the thread-change reset effect will open the sidebar instead of closing it.
Expand Down Expand Up @@ -1532,7 +1561,7 @@
);

const focusComposer = useCallback(() => {
composerRef.current?.focusAtEnd();

Check warning on line 1564 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
}, []);
const scheduleComposerFocus = useCallback(() => {
window.requestAnimationFrame(() => {
Expand All @@ -1540,7 +1569,7 @@
});
}, [focusComposer]);
const addTerminalContextToDraft = useCallback((selection: TerminalContextSelection) => {
composerRef.current?.addTerminalContext(selection);

Check warning on line 1572 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
}, []);
const setTerminalOpen = useCallback(
(open: boolean) => {
Expand Down Expand Up @@ -1894,6 +1923,13 @@
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: {
Expand Down Expand Up @@ -2691,7 +2727,7 @@
};
});
promptRef.current = "";
composerRef.current?.resetCursorState({ cursor: 0 });

Check warning on line 2730 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
},
[activePendingProgress?.activeQuestion, activePendingUserInput],
);
Expand All @@ -2718,7 +2754,7 @@
),
},
}));
const snapshot = composerRef.current?.readSnapshot();

Check warning on line 2757 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (
snapshot?.value !== value ||
snapshot.cursor !== nextCursor ||
Expand Down Expand Up @@ -2781,7 +2817,7 @@
return;
}

const sendCtx = composerRef.current?.getSendContext();

Check warning on line 2820 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (!sendCtx) {
return;
}
Expand Down Expand Up @@ -2916,7 +2952,7 @@
return;
}

const sendCtx = composerRef.current?.getSendContext();

Check warning on line 2955 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (!sendCtx) {
return;
}
Expand Down Expand Up @@ -3333,22 +3369,16 @@
{/* end chat column */}

{/* Plan sidebar */}
{planSidebarOpen ? (
{planSidebarOpen && !shouldUsePlanSidebarSheet ? (
<PlanSidebar
activePlan={activePlan}
activeProposedPlan={sidebarProposedPlan}
environmentId={environmentId}
markdownCwd={gitCwd ?? undefined}
workspaceRoot={activeWorkspaceRoot}
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 ?? sidebarProposedPlan?.turnId ?? null;
if (turnKey) {
planSidebarDismissedForTurnRef.current = turnKey;
}
}}
mode="sidebar"
onClose={closePlanSidebar}
/>
) : null}
</div>
Expand All @@ -3370,6 +3400,20 @@
onAddTerminalContext={addTerminalContextToDraft}
/>
))}
{shouldUsePlanSidebarSheet ? (
<PlanSidebarSheet open={planSidebarOpen} onClose={closePlanSidebar}>
<PlanSidebar
activePlan={activePlan}
activeProposedPlan={sidebarProposedPlan}
environmentId={environmentId}
markdownCwd={gitCwd ?? undefined}
workspaceRoot={activeWorkspaceRoot}
timestampFormat={timestampFormat}
mode="sheet"
onClose={closePlanSidebar}
/>
</PlanSidebarSheet>
) : null}

{expandedImage && (
<ExpandedImageDialog preview={expandedImage} onClose={closeExpandedImage} />
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 @@ -58,6 +58,7 @@ interface PlanSidebarProps {
markdownCwd: string | undefined;
workspaceRoot: string | undefined;
timestampFormat: TimestampFormat;
mode?: "sheet" | "sidebar";
onClose: () => void;
}

Expand All @@ -68,6 +69,7 @@ const PlanSidebar = memo(function PlanSidebar({
markdownCwd,
workspaceRoot,
timestampFormat,
mode = "sidebar",
onClose,
}: PlanSidebarProps) {
const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false);
Expand Down Expand Up @@ -121,7 +123,14 @@ const PlanSidebar = memo(function PlanSidebar({
}, [environmentId, 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.$environmentId.$threadId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ 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";
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 @@ -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}
</SheetPopup>
Expand Down Expand Up @@ -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,
Expand Down
Loading