Skip to content
Merged
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
20 changes: 6 additions & 14 deletions apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import { useEffect, type ReactNode } from "react";
import { useNavigate } from "@tanstack/react-router";

import ThreadSidebar from "./Sidebar";
import { RightPanelSheet } from "./RightPanelSheet";
import { Sidebar, SidebarProvider, SidebarRail } from "./ui/sidebar";
import { useMediaQuery } from "../hooks/useMediaQuery";
import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout";
import { PixsoPanel, PixsoPanelInlineSidebar, usePixsoStore } from "../ru-fork/pixso-move";
import { OverlayPanelHost } from "../ru-fork/rightPanel";
import {
clearShortcutModifierState,
syncShortcutModifierStateFromKeyboardEvent,
Expand All @@ -17,9 +14,6 @@ const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;
export function AppSidebarLayout({ children }: { children: ReactNode }) {
const navigate = useNavigate();
const pixsoOpen = usePixsoStore((state) => state.panelOpen);
const closePixso = usePixsoStore((state) => state.closePanel);
const useRightPanelSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);

useEffect(() => {
const onWindowKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -77,13 +71,11 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
<SidebarRail />
</Sidebar>
{children}
{useRightPanelSheet ? (
<RightPanelSheet open={pixsoOpen} onClose={closePixso}>
<PixsoPanel onClose={closePixso} mode="sheet" />
</RightPanelSheet>
) : (
<PixsoPanelInlineSidebar open={pixsoOpen} onClose={closePixso} />
)}
{/* ru-fork: one shared host for all global overlay panels, mounted once outside
the routes so it never remounts on navigation. The coordinator decides which
entry is shown; the host keeps the slot open across swaps and keeps content
mounted through the close slide. */}
<OverlayPanelHost />
</SidebarProvider>
);
}
31 changes: 6 additions & 25 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ import {
import { sanitizeThreadErrorMessage } from "~/rpc/transportError";
import { retainThreadDetailSubscription } from "../environments/runtime/service";
import { RightPanelSheet } from "./RightPanelSheet";
import { useMcpManagerStore } from "../ru-fork/mcp-manage";
import { useRightPanelStore } from "../ru-fork/rightPanel";

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 @@ -1596,17 +1596,17 @@ export default function ChatView(props: ChatViewProps) {
() => shortcutLabelForCommand(keybindings, "diff.toggle", nonTerminalShortcutLabelOptions),
[keybindings, nonTerminalShortcutLabelOptions],
);
const mcpPanelOpen = useMcpManagerStore((state) => state.panelOpen);
const setMcpPanelOpen = useMcpManagerStore((state) => state.setPanelOpen);
const closeOverlayPanel = useRightPanelStore((state) => state.close);

const onToggleDiff = useCallback(() => {
if (!isServerThread) {
return;
}
if (!diffOpen) {
onDiffPanelOpen?.();
// Diff and MCP share the right-panel slot: opening diff closes MCP.
setMcpPanelOpen(false);
// Diff and the overlay panels share the right-panel slot: opening diff
// closes whichever overlay (MCP/Pixso) is open.
closeOverlayPanel();
}
void navigate({
to: "/$environmentId/$threadId",
Expand All @@ -1626,27 +1626,10 @@ export default function ChatView(props: ChatViewProps) {
isServerThread,
navigate,
onDiffPanelOpen,
setMcpPanelOpen,
closeOverlayPanel,
threadId,
]);

const onToggleMcp = useCallback(() => {
const next = !mcpPanelOpen;
setMcpPanelOpen(next);
// Mutual exclusion with the diff panel: opening MCP closes diff.
if (next && diffOpen) {
void navigate({
to: "/$environmentId/$threadId",
params: { environmentId, threadId },
replace: true,
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return { ...rest, diff: undefined };
},
});
}
}, [diffOpen, environmentId, mcpPanelOpen, navigate, setMcpPanelOpen, threadId]);

const envLocked = Boolean(
activeThread &&
(activeThread.messages.length > 0 ||
Expand Down Expand Up @@ -3634,7 +3617,6 @@ export default function ChatView(props: ChatViewProps) {
diffToggleShortcutLabel={diffPanelShortcutLabel}
gitCwd={gitCwd}
diffOpen={diffOpen}
mcpPanelOpen={mcpPanelOpen}
advancedChatOpen={advancedChatOpen}
onToggleAdvancedChat={() => {
if (activeThread) toggleAdvancedChatMode(activeThread.id);
Expand All @@ -3645,7 +3627,6 @@ export default function ChatView(props: ChatViewProps) {
onDeleteProjectScript={deleteProjectScript}
onToggleTerminal={toggleTerminalVisibility}
onToggleDiff={onToggleDiff}
onToggleMcp={onToggleMcp}
/>
</header>

Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/DiffPanelShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export function DiffPanelShell(props: {
mode: DiffPanelMode;
header: ReactNode;
children: ReactNode;
/** Optional override for the shell's surface (e.g. `bg-card`); defaults to `bg-background`. */
className?: string;
}) {
const shouldUseDragRegion = isElectron && props.mode !== "sheet";

Expand All @@ -31,6 +33,7 @@ export function DiffPanelShell(props: {
props.mode === "inline"
? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
: "w-full",
props.className,
)}
>
{shouldUseDragRegion ? (
Expand Down
52 changes: 52 additions & 0 deletions apps/web/src/components/RightInlineSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { CSSProperties, ReactNode } from "react";

import { Sidebar, SidebarProvider, SidebarRail } from "./ui/sidebar";

// ru-fork: single generic shell for the right-side inline sidebars (diff + overlays).
// Replaces three byte-identical wrappers — callers differ only in width constants,
// the storage key, an optional shouldAcceptWidth guard, and the (already open-gated)
// children they render.
export function RightInlineSidebar({
open,
onOpenChange,
storageKey,
defaultWidth,
minWidth,
maxWidth,
shouldAcceptWidth,
children,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
storageKey: string;
defaultWidth: string;
minWidth: number;
maxWidth: number;
shouldAcceptWidth?: (args: { nextWidth: number; wrapper: HTMLElement }) => boolean;
children: ReactNode;
}) {
return (
<SidebarProvider
defaultOpen={false}
open={open}
onOpenChange={onOpenChange}
className="min-h-0 w-auto flex-none bg-transparent"
style={{ "--sidebar-width": defaultWidth } as CSSProperties}
>
<Sidebar
side="right"
collapsible="offcanvas"
className="border-l border-border bg-card text-foreground"
resizable={{
maxWidth,
minWidth,
...(shouldAcceptWidth ? { shouldAcceptWidth } : {}),
storageKey,
}}
>
{children}
<SidebarRail />
</Sidebar>
</SidebarProvider>
);
}
23 changes: 1 addition & 22 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { scopeThreadRef } from "@t3tools/client-runtime";
import { memo } from "react";
import GitActionsControl from "../GitActionsControl";
import { type DraftId } from "~/composerDraftStore";
import { BoxesIcon, DiffIcon, SparklesIcon, TerminalSquareIcon } from "lucide-react";
import { DiffIcon, SparklesIcon, TerminalSquareIcon } from "lucide-react";
import { Badge } from "../ui/badge";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl";
Expand All @@ -36,7 +36,6 @@ interface ChatHeaderProps {
diffToggleShortcutLabel: string | null;
gitCwd: string | null;
diffOpen: boolean;
mcpPanelOpen: boolean;
// ru-fork: advanced chat mode (qwen transcript view) toggle.
advancedChatOpen: boolean;
onToggleAdvancedChat: () => void;
Expand All @@ -46,7 +45,6 @@ interface ChatHeaderProps {
onDeleteProjectScript: (scriptId: string) => Promise<void>;
onToggleTerminal: () => void;
onToggleDiff: () => void;
onToggleMcp: () => void;
}

export const ChatHeader = memo(function ChatHeader({
Expand All @@ -67,7 +65,6 @@ export const ChatHeader = memo(function ChatHeader({
diffToggleShortcutLabel,
gitCwd,
diffOpen,
mcpPanelOpen,
advancedChatOpen,
onToggleAdvancedChat,
onRunProjectScript,
Expand All @@ -76,7 +73,6 @@ export const ChatHeader = memo(function ChatHeader({
onDeleteProjectScript,
onToggleTerminal,
onToggleDiff,
onToggleMcp,
}: ChatHeaderProps) {
const primaryEnvironmentId = usePrimaryEnvironmentId();
const isRemoteEnvironment =
Expand Down Expand Up @@ -177,23 +173,6 @@ export const ChatHeader = memo(function ChatHeader({
: "Показать/скрыть панель diff"}
</TooltipPopup>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Toggle
className="shrink-0"
pressed={mcpPanelOpen}
onPressedChange={onToggleMcp}
aria-label="Toggle MCP panel"
variant="outline"
size="xs"
>
<BoxesIcon className="size-3" />
</Toggle>
}
/>
<TooltipPopup side="bottom">Показать/скрыть панель MCP-серверов</TooltipPopup>
</Tooltip>
{/* ru-fork: advanced chat mode — render the CLI transcript view. */}
<Tooltip>
<TooltipTrigger
Expand Down
58 changes: 29 additions & 29 deletions apps/web/src/routes/_chat.$environmentId.$threadId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../st
import { createThreadSelectorByRef } from "../storeSelectors";
import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes";
import { RightPanelSheet } from "../components/RightPanelSheet";
import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar";
import { useMcpManagerStore } from "../ru-fork/mcp-manage";
import { RightInlineSidebar } from "../components/RightInlineSidebar";
import { SidebarInset } from "../components/ui/sidebar";
import { useRightPanelStore } from "../ru-fork/rightPanel";

const DiffPanel = lazy(() => import("../components/DiffPanel"));
const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width";
Expand Down Expand Up @@ -114,28 +115,17 @@ const DiffPanelInlineSidebar = (props: {
);

return (
<SidebarProvider
defaultOpen={false}
<RightInlineSidebar
open={diffOpen}
onOpenChange={onOpenChange}
className="w-auto min-h-0 flex-none bg-transparent"
style={{ "--sidebar-width": DIFF_INLINE_DEFAULT_WIDTH } as React.CSSProperties}
storageKey={DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY}
defaultWidth={DIFF_INLINE_DEFAULT_WIDTH}
minWidth={DIFF_INLINE_SIDEBAR_MIN_WIDTH}
maxWidth={DIFF_INLINE_SIDEBAR_MAX_WIDTH}
shouldAcceptWidth={shouldAcceptInlineSidebarWidth}
>
<Sidebar
side="right"
collapsible="offcanvas"
className="border-l border-border bg-card text-foreground"
resizable={{
maxWidth: DIFF_INLINE_SIDEBAR_MAX_WIDTH,
minWidth: DIFF_INLINE_SIDEBAR_MIN_WIDTH,
shouldAcceptWidth: shouldAcceptInlineSidebarWidth,
storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY,
}}
>
{renderDiffContent ? <LazyDiffPanel mode="sidebar" /> : null}
<SidebarRail />
</Sidebar>
</SidebarProvider>
{renderDiffContent ? <LazyDiffPanel mode="sidebar" /> : null}
</RightInlineSidebar>
);
};

Expand Down Expand Up @@ -169,9 +159,12 @@ function ChatThreadRouteView() {
const serverThreadStarted = threadHasStarted(serverThread);
const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads;
const diffOpen = search.diff === "1";
// MCP panel open/close is global (useMcpManagerStore) and rendered once in the
// _chat layout; here we only need the setter to keep diff + MCP mutually exclusive.
const setMcpPanelOpen = useMcpManagerStore((store) => store.setPanelOpen);
// The overlay panels (MCP/Pixso) are coordinator-driven and mounted in
// AppSidebarLayout. Diff stays URL-driven; the two seams below keep all three
// mutually exclusive: opening diff closes the overlay, and the effect closes
// diff whenever an overlay opens (e.g. from the left nav).
const overlayPanelOpen = useRightPanelStore((store) => store.open !== null);
const closeOverlayPanel = useRightPanelStore((store) => store.close);
const shouldUseDiffSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);
const currentThreadKey = threadRef ? `${threadRef.environmentId}:${threadRef.threadId}` : null;
const [diffPanelMountState, setDiffPanelMountState] = useState(() => ({
Expand Down Expand Up @@ -208,8 +201,8 @@ function ChatThreadRouteView() {
return;
}
markDiffOpened();
// Diff and MCP share the right-panel slot — only one open at a time.
setMcpPanelOpen(false);
// Diff and the overlay panels share the right-panel slot — only one open.
closeOverlayPanel();
void navigate({
to: "/$environmentId/$threadId",
params: buildThreadRouteParams(threadRef),
Expand All @@ -218,7 +211,14 @@ function ChatThreadRouteView() {
return { ...rest, diff: "1" };
},
});
}, [markDiffOpened, navigate, setMcpPanelOpen, threadRef]);
}, [markDiffOpened, navigate, closeOverlayPanel, threadRef]);

// Seam: opening an overlay panel (from the left nav) closes the diff panel.
useEffect(() => {
if (overlayPanelOpen && diffOpen) {
closeDiff();
}
}, [overlayPanelOpen, diffOpen, closeDiff]);

useEffect(() => {
if (!threadRef || !bootstrapComplete) {
Expand Down Expand Up @@ -261,7 +261,7 @@ function ChatThreadRouteView() {
onOpenDiff={openDiff}
renderDiffContent={shouldRenderDiffContent}
/>
{/* ru-fork: the MCP panel is mounted once in the _chat layout (McpPanelMount). */}
{/* ru-fork: the MCP/Pixso overlay panels are mounted in AppSidebarLayout, not per route. */}
</>
);
}
Expand All @@ -279,7 +279,7 @@ function ChatThreadRouteView() {
<RightPanelSheet open={diffOpen} onClose={closeDiff}>
{shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null}
</RightPanelSheet>
{/* ru-fork: the MCP panel sheet is mounted once in the _chat layout. */}
{/* ru-fork: the MCP/Pixso overlay panels are mounted in AppSidebarLayout, not per route. */}
</>
);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/routes/_chat.draft.$draftId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ function DraftChatThreadRouteView() {
return null;
}

// The MCP panel is mounted once in the _chat layout (McpPanelMount), so it is
// available here on drafts too without a per-route mount.
// The MCP + Pixso overlay panels are mounted once in AppSidebarLayout
// (coordinator-driven), so they are available here on drafts too.
return (
<SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh">
<ChatView
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/routes/_chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic";
import { useSettings } from "~/hooks/useSettings";
import { useServerKeybindings } from "~/rpc/serverState";
import { useActiveProjectRef } from "~/hooks/useActiveProject";
import { McpPanelMount } from "../ru-fork/mcp-manage";
import { getPrimaryEnvironmentConnection } from "../environments/runtime";
import { getPrimaryKnownEnvironment } from "../environments/primary";

Expand Down Expand Up @@ -124,9 +123,8 @@ function ChatRouteLayout() {
<ChatRouteGlobalShortcuts />
<McpActiveProjectSync />
<Outlet />
{/* ru-fork: single MCP panel mount for all chat routes (draft + thread) —
hoisted here so it never remounts on navigation. */}
<McpPanelMount />
{/* ru-fork: the MCP + Pixso overlay panels are mounted once in
AppSidebarLayout (coordinator-driven), not here. */}
</>
);
}
Expand Down
Loading
Loading