diff --git a/studio/frontend/src/features/chat/chat-settings-sheet.tsx b/studio/frontend/src/features/chat/chat-settings-sheet.tsx index 6070bd2e40..fd41e558f9 100644 --- a/studio/frontend/src/features/chat/chat-settings-sheet.tsx +++ b/studio/frontend/src/features/chat/chat-settings-sheet.tsx @@ -18,6 +18,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { InfoHint } from "@/components/ui/info-hint"; +import { PanelResizeHandle } from "@/components/ui/panel-resize-handle"; import { InputGroup, InputGroupAddon, @@ -44,7 +45,13 @@ import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; import { NumericValueInput, snapToStep } from "@/features/model-picker"; import { RetrievalSettingsSection } from "@/features/rag"; import { useLlamaUpdateCheck } from "@/hooks/use-llama-update-check"; +import { + CHAT_SETTINGS_WIDTH_MIN, + clampChatSettingsWidth, + useChatSettingsWidth, +} from "@/hooks/use-chat-settings-width"; import { useIsMobile } from "@/hooks/use-mobile"; +import { useT } from "@/i18n"; import { ChevronDownStandardIcon } from "@/lib/chevron-icons"; import { toast } from "@/lib/toast"; import { cn } from "@/lib/utils"; @@ -52,7 +59,7 @@ import { Edit03Icon, LayoutAlignRightIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Braces, ChevronDown, ExternalLink } from "lucide-react"; import { Tooltip as TooltipPrimitive } from "radix-ui"; -import { Fragment, type ReactNode } from "react"; +import { type CSSProperties, Fragment, type ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { OpenAICodeExecSection } from "./components/openai-code-exec-section"; import { PermissionModeDropdown } from "./permission-mode-select"; @@ -363,6 +370,15 @@ export function ChatSettingsPanel({ onExternalProviderChange, externalProviderType = null, }: ChatSettingsPanelProps) { + const asideRef = useRef(null); + const t = useT(); + const { + width: settingsWidth, + max: settingsMax, + stored: settingsStored, + setWidth: setSettingsWidth, + resetWidth: resetSettingsWidth, + } = useChatSettingsWidth(); // Local models show every knob; providerCapabilities is only consulted when // isExternalModel. Unknown providers fall back to the OpenAI-compat shape via // getProviderCapabilities, so these flags never undercount support. @@ -461,6 +477,23 @@ export function ChatSettingsPanel({ // When the prompt overflows the inline box, clicking opens the popup editor. const systemPromptBoxRef = useRef(null); const [systemPromptOverflows, setSystemPromptOverflows] = useState(false); + const promptObserverRef = useRef(null); + const measurePromptRef = useRef<() => void>(() => {}); + // The section unmounts its textarea when collapsed, so observe through a + // callback ref: a stored observer would cling to the detached node and the + // remounted one would never be measured. + const attachPromptBox = useCallback((node: HTMLTextAreaElement | null) => { + systemPromptBoxRef.current = node; + promptObserverRef.current?.disconnect(); + promptObserverRef.current = null; + if (!node || typeof ResizeObserver === "undefined") return; + // Resizing rewraps the prompt, and a drag changes the width through a + // custom property without re-rendering, so watch the box itself. + const observer = new ResizeObserver(() => measurePromptRef.current()); + observer.observe(node); + promptObserverRef.current = observer; + measurePromptRef.current(); + }, []); const [activePresetBaseline, setActivePresetBaseline] = useState(params); const presets = useMemo(() => { return getOrderedPresets(customPresets); @@ -746,15 +779,20 @@ export function ChatSettingsPanel({ }, [open]); useEffect(() => { - const el = systemPromptBoxRef.current; - setSystemPromptOverflows( - currentSystemPrompt.length > 0 && - el != null && - el.clientHeight > 0 && - el.scrollHeight > el.clientHeight + 1, - ); + measurePromptRef.current = () => { + const el = systemPromptBoxRef.current; + setSystemPromptOverflows( + currentSystemPrompt.length > 0 && + el != null && + el.clientHeight > 0 && + el.scrollHeight > el.clientHeight + 1, + ); + }; + measurePromptRef.current(); }, [currentSystemPrompt, open]); + useEffect(() => () => promptObserverRef.current?.disconnect(), []); + const settingsScrollRef = useRef(null); const settingsContent = ( @@ -1124,7 +1162,7 @@ export function ChatSettingsPanel({ )} >