diff --git a/src/components/QuickSettingsPanel.jsx b/src/components/QuickSettingsPanel.jsx
deleted file mode 100644
index 5dd57284..00000000
--- a/src/components/QuickSettingsPanel.jsx
+++ /dev/null
@@ -1,448 +0,0 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
-import {
- ChevronLeft,
- ChevronRight,
- Maximize2,
- Eye,
- Settings2,
- Moon,
- Sun,
- ArrowDown,
- Mic,
- Brain,
- Sparkles,
- FileText,
- Languages,
- GripVertical
-} from 'lucide-react';
-import { useTranslation } from 'react-i18next';
-import { DarkModeToggle } from '../shared/view/ui';
-
-import { useUiPreferences } from '../hooks/useUiPreferences';
-import { useTheme } from '../contexts/ThemeContext';
-import LanguageSelector from './LanguageSelector';
-
-import { useDeviceSettings } from '../hooks/useDeviceSettings';
-
-
-const QuickSettingsPanel = () => {
- const { t } = useTranslation('settings');
- const [isOpen, setIsOpen] = useState(false);
- const [whisperMode, setWhisperMode] = useState(() => {
- return localStorage.getItem('whisperMode') || 'default';
- });
- const { isDarkMode } = useTheme();
-
- const { isMobile } = useDeviceSettings({ trackPWA: false });
-
- const { preferences, setPreference } = useUiPreferences();
- const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences;
-
- // Draggable handle state
- const [handlePosition, setHandlePosition] = useState(() => {
- const saved = localStorage.getItem('quickSettingsHandlePosition');
- if (saved) {
- try {
- const parsed = JSON.parse(saved);
- return parsed.y ?? 50;
- } catch {
- // Remove corrupted data
- localStorage.removeItem('quickSettingsHandlePosition');
- return 50;
- }
- }
- return 50; // Default to 50% (middle of screen)
- });
-
- const [isDragging, setIsDragging] = useState(false);
- const [dragStartY, setDragStartY] = useState(0);
- const [dragStartPosition, setDragStartPosition] = useState(0);
- const [hasMoved, setHasMoved] = useState(false); // Track if user has moved during drag
- const handleRef = useRef(null);
- const constraintsRef = useRef({ min: 10, max: 90 }); // Percentage constraints
- const dragThreshold = 5; // Pixels to move before it's considered a drag
-
- // Save handle position to localStorage when it changes
- useEffect(() => {
- localStorage.setItem('quickSettingsHandlePosition', JSON.stringify({ y: handlePosition }));
- }, [handlePosition]);
-
- // Calculate position from percentage
- const getPositionStyle = useCallback(() => {
- if (isMobile) {
- // On mobile, convert percentage to pixels from bottom
- const bottomPixels = (window.innerHeight * handlePosition) / 100;
- return { bottom: `${bottomPixels}px` };
- } else {
- // On desktop, use top with percentage
- return { top: `${handlePosition}%`, transform: 'translateY(-50%)' };
- }
- }, [handlePosition, isMobile]);
-
- // Handle mouse/touch start
- const handleDragStart = useCallback((e) => {
- // Don't prevent default yet - we want to allow click if no drag happens
- e.stopPropagation();
-
- const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
- setDragStartY(clientY);
- setDragStartPosition(handlePosition);
- setHasMoved(false);
- setIsDragging(false); // Don't set dragging until threshold is passed
- }, [handlePosition]);
-
- // Handle mouse/touch move
- const handleDragMove = useCallback((e) => {
- if (dragStartY === 0) return; // Not in a potential drag
-
- const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
- const deltaY = Math.abs(clientY - dragStartY);
-
- // Check if we've moved past threshold
- if (!isDragging && deltaY > dragThreshold) {
- setIsDragging(true);
- setHasMoved(true);
- document.body.style.cursor = 'grabbing';
- document.body.style.userSelect = 'none';
-
- // Prevent body scroll on mobile during drag
- if (e.type.includes('touch')) {
- document.body.style.overflow = 'hidden';
- document.body.style.position = 'fixed';
- document.body.style.width = '100%';
- }
- }
-
- if (!isDragging) return;
-
- // Prevent scrolling on touch move
- if (e.type.includes('touch')) {
- e.preventDefault();
- }
-
- const actualDeltaY = clientY - dragStartY;
-
- // For top-based positioning (desktop), moving down increases top percentage
- // For bottom-based positioning (mobile), we need to invert
- let percentageDelta;
- if (isMobile) {
- // On mobile, moving down should decrease bottom position (increase percentage from top)
- percentageDelta = -(actualDeltaY / window.innerHeight) * 100;
- } else {
- // On desktop, moving down should increase top position
- percentageDelta = (actualDeltaY / window.innerHeight) * 100;
- }
-
- let newPosition = dragStartPosition + percentageDelta;
-
- // Apply constraints
- newPosition = Math.max(constraintsRef.current.min, Math.min(constraintsRef.current.max, newPosition));
-
- setHandlePosition(newPosition);
- }, [isDragging, dragStartY, dragStartPosition, isMobile, dragThreshold]);
-
- // Handle mouse/touch end
- const handleDragEnd = useCallback(() => {
- setIsDragging(false);
- setDragStartY(0);
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
-
- // Restore body scroll on mobile
- document.body.style.overflow = '';
- document.body.style.position = '';
- document.body.style.width = '';
- }, []);
-
- // Cleanup body styles on unmount in case component unmounts while dragging
- useEffect(() => {
- return () => {
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- document.body.style.overflow = '';
- document.body.style.position = '';
- document.body.style.width = '';
- };
- }, []);
-
- // Set up global event listeners for drag
- useEffect(() => {
- if (dragStartY !== 0) {
- // Mouse events
- const handleMouseMove = (e) => handleDragMove(e);
- const handleMouseUp = () => handleDragEnd();
-
- // Touch events
- const handleTouchMove = (e) => handleDragMove(e);
- const handleTouchEnd = () => handleDragEnd();
-
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- document.addEventListener('touchmove', handleTouchMove, { passive: false });
- document.addEventListener('touchend', handleTouchEnd);
-
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- document.removeEventListener('touchmove', handleTouchMove);
- document.removeEventListener('touchend', handleTouchEnd);
- };
- }
- }, [dragStartY, handleDragMove, handleDragEnd]);
-
- const handleToggle = (e) => {
- // Don't toggle if user was dragging
- if (hasMoved) {
- e.preventDefault();
- setHasMoved(false);
- return;
- }
-
- setIsOpen((previous) => !previous);
- };
-
- return (
- <>
- {/* Pull Tab - Combined drag handle and toggle button */}
-
-
- {/* Panel */}
-
-
- {/* Header */}
-
-
-
- {t('quickSettings.title')}
-
-
-
- {/* Settings Content */}
-
- {/* Appearance Settings */}
-
-
{t('quickSettings.sections.appearance')}
-
-
-
- {isDarkMode ? : }
- {t('quickSettings.darkMode')}
-
-
-
-
- {/* Language Selector */}
-
-
-
-
-
- {/* Tool Display Settings */}
-
-
{t('quickSettings.sections.toolDisplay')}
-
-
-
-
-
-
-
- {/* View Options */}
-
-
{t('quickSettings.sections.viewOptions')}
-
-
-
-
- {/* Input Settings */}
-
-
{t('quickSettings.sections.inputSettings')}
-
-
-
- {t('quickSettings.sendByCtrlEnterDescription')}
-
-
-
- {/* Whisper Dictation Settings - HIDDEN */}
-
-
{t('quickSettings.sections.whisperDictation')}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Backdrop */}
- {isOpen && (
-
- )}
- >
- );
-};
-
-export default QuickSettingsPanel;
diff --git a/src/components/QuickSettingsPanel.tsx b/src/components/QuickSettingsPanel.tsx
new file mode 100644
index 00000000..564628fe
--- /dev/null
+++ b/src/components/QuickSettingsPanel.tsx
@@ -0,0 +1,5 @@
+import QuickSettingsPanelView from './quick-settings-panel/view/QuickSettingsPanelView';
+
+export default function QuickSettingsPanel() {
+ return ;
+}
diff --git a/src/components/quick-settings-panel/constants.ts b/src/components/quick-settings-panel/constants.ts
new file mode 100644
index 00000000..5f1a8e21
--- /dev/null
+++ b/src/components/quick-settings-panel/constants.ts
@@ -0,0 +1,93 @@
+import {
+ ArrowDown,
+ Brain,
+ Eye,
+ FileText,
+ Languages,
+ Maximize2,
+ Mic,
+ Sparkles,
+} from 'lucide-react';
+import type {
+ PreferenceToggleItem,
+ WhisperMode,
+ WhisperOption,
+} from './types';
+
+export const HANDLE_POSITION_STORAGE_KEY = 'quickSettingsHandlePosition';
+export const WHISPER_MODE_STORAGE_KEY = 'whisperMode';
+export const WHISPER_MODE_CHANGED_EVENT = 'whisperModeChanged';
+
+export const DEFAULT_HANDLE_POSITION = 50;
+export const HANDLE_POSITION_MIN = 10;
+export const HANDLE_POSITION_MAX = 90;
+export const DRAG_THRESHOLD_PX = 5;
+
+export const SETTING_ROW_CLASS =
+ 'flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600';
+
+export const TOGGLE_ROW_CLASS = `${SETTING_ROW_CLASS} cursor-pointer`;
+
+export const CHECKBOX_CLASS =
+ 'h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600';
+
+export const TOOL_DISPLAY_TOGGLES: PreferenceToggleItem[] = [
+ {
+ key: 'autoExpandTools',
+ labelKey: 'quickSettings.autoExpandTools',
+ icon: Maximize2,
+ },
+ {
+ key: 'showRawParameters',
+ labelKey: 'quickSettings.showRawParameters',
+ icon: Eye,
+ },
+ {
+ key: 'showThinking',
+ labelKey: 'quickSettings.showThinking',
+ icon: Brain,
+ },
+];
+
+export const VIEW_OPTION_TOGGLES: PreferenceToggleItem[] = [
+ {
+ key: 'autoScrollToBottom',
+ labelKey: 'quickSettings.autoScrollToBottom',
+ icon: ArrowDown,
+ },
+];
+
+export const INPUT_SETTING_TOGGLES: PreferenceToggleItem[] = [
+ {
+ key: 'sendByCtrlEnter',
+ labelKey: 'quickSettings.sendByCtrlEnter',
+ icon: Languages,
+ },
+];
+
+export const WHISPER_OPTIONS: WhisperOption[] = [
+ {
+ value: 'default',
+ titleKey: 'quickSettings.whisper.modes.default',
+ descriptionKey: 'quickSettings.whisper.modes.defaultDescription',
+ icon: Mic,
+ },
+ {
+ value: 'prompt',
+ titleKey: 'quickSettings.whisper.modes.prompt',
+ descriptionKey: 'quickSettings.whisper.modes.promptDescription',
+ icon: Sparkles,
+ },
+ {
+ value: 'vibe',
+ titleKey: 'quickSettings.whisper.modes.vibe',
+ descriptionKey: 'quickSettings.whisper.modes.vibeDescription',
+ icon: FileText,
+ },
+];
+
+export const VIBE_MODE_ALIASES: WhisperMode[] = [
+ 'vibe',
+ 'instructions',
+ 'architect',
+];
diff --git a/src/components/quick-settings-panel/hooks/useQuickSettingsDrag.ts b/src/components/quick-settings-panel/hooks/useQuickSettingsDrag.ts
new file mode 100644
index 00000000..1913d668
--- /dev/null
+++ b/src/components/quick-settings-panel/hooks/useQuickSettingsDrag.ts
@@ -0,0 +1,239 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
+import {
+ DEFAULT_HANDLE_POSITION,
+ DRAG_THRESHOLD_PX,
+ HANDLE_POSITION_MAX,
+ HANDLE_POSITION_MIN,
+ HANDLE_POSITION_STORAGE_KEY,
+} from '../constants';
+import type { QuickSettingsHandleStyle } from '../types';
+
+type UseQuickSettingsDragProps = {
+ isMobile: boolean;
+};
+
+type StartDragEvent = ReactMouseEvent | ReactTouchEvent;
+type MoveDragEvent = MouseEvent | TouchEvent;
+type EventWithClientY = StartDragEvent | MoveDragEvent;
+
+const clampPosition = (value: number): number => (
+ Math.max(HANDLE_POSITION_MIN, Math.min(HANDLE_POSITION_MAX, value))
+);
+
+const readHandlePosition = (): number => {
+ if (typeof window === 'undefined') {
+ return DEFAULT_HANDLE_POSITION;
+ }
+
+ const saved = localStorage.getItem(HANDLE_POSITION_STORAGE_KEY);
+ if (!saved) {
+ return DEFAULT_HANDLE_POSITION;
+ }
+
+ try {
+ const parsed = JSON.parse(saved) as { y?: unknown };
+ if (typeof parsed.y === 'number' && Number.isFinite(parsed.y)) {
+ return clampPosition(parsed.y);
+ }
+ } catch {
+ localStorage.removeItem(HANDLE_POSITION_STORAGE_KEY);
+ return DEFAULT_HANDLE_POSITION;
+ }
+
+ return DEFAULT_HANDLE_POSITION;
+};
+
+const isTouchEvent = (event: { type: string }): boolean => event.type.includes('touch');
+
+const getClientY = (event: EventWithClientY): number | null => {
+ if ('touches' in event) {
+ return event.touches[0]?.clientY ?? null;
+ }
+
+ return 'clientY' in event && typeof event.clientY === 'number'
+ ? event.clientY
+ : null;
+};
+
+export function useQuickSettingsDrag({ isMobile }: UseQuickSettingsDragProps) {
+ const [handlePosition, setHandlePosition] = useState(readHandlePosition);
+ const [isPointerDown, setIsPointerDown] = useState(false);
+ const [isDragging, setIsDragging] = useState(false);
+
+ const dragStartYRef = useRef(null);
+ const dragStartPositionRef = useRef(DEFAULT_HANDLE_POSITION);
+ const didDragRef = useRef(false);
+ const suppressNextClickRef = useRef(false);
+ const bodyStylesAppliedRef = useRef(false);
+
+ const clearBodyDragStyles = useCallback(() => {
+ if (!bodyStylesAppliedRef.current) {
+ return;
+ }
+
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ document.body.style.overflow = '';
+ document.body.style.position = '';
+ document.body.style.width = '';
+ bodyStylesAppliedRef.current = false;
+ }, []);
+
+ const applyBodyDragStyles = useCallback((isTouchDragging: boolean) => {
+ if (bodyStylesAppliedRef.current) {
+ return;
+ }
+
+ document.body.style.cursor = 'grabbing';
+ document.body.style.userSelect = 'none';
+
+ // Touch drag should lock body scroll so the handle movement stays smooth.
+ if (isTouchDragging) {
+ document.body.style.overflow = 'hidden';
+ document.body.style.position = 'fixed';
+ document.body.style.width = '100%';
+ }
+
+ bodyStylesAppliedRef.current = true;
+ }, []);
+
+ const endDrag = useCallback(() => {
+ if (!isPointerDown && dragStartYRef.current === null) {
+ return;
+ }
+
+ suppressNextClickRef.current = didDragRef.current;
+ didDragRef.current = false;
+ dragStartYRef.current = null;
+ setIsPointerDown(false);
+ setIsDragging(false);
+ clearBodyDragStyles();
+ }, [clearBodyDragStyles, isPointerDown]);
+
+ const handleMove = useCallback(
+ (event: MoveDragEvent) => {
+ if (!isPointerDown || dragStartYRef.current === null) {
+ return;
+ }
+
+ const clientY = getClientY(event);
+ if (clientY === null) {
+ return;
+ }
+
+ const rawDelta = clientY - dragStartYRef.current;
+ const movedPastThreshold = Math.abs(rawDelta) > DRAG_THRESHOLD_PX;
+
+ if (!didDragRef.current && movedPastThreshold) {
+ didDragRef.current = true;
+ setIsDragging(true);
+ applyBodyDragStyles(isTouchEvent(event));
+ }
+
+ if (!didDragRef.current) {
+ return;
+ }
+
+ if (isTouchEvent(event)) {
+ event.preventDefault();
+ }
+
+ const viewportHeight = Math.max(window.innerHeight, 1);
+ const normalizedDelta = (rawDelta / viewportHeight) * 100;
+ const positionDelta = isMobile ? -normalizedDelta : normalizedDelta;
+ setHandlePosition(clampPosition(dragStartPositionRef.current + positionDelta));
+ },
+ [applyBodyDragStyles, isMobile, isPointerDown],
+ );
+
+ const startDrag = useCallback((event: StartDragEvent) => {
+ event.stopPropagation();
+
+ const clientY = getClientY(event);
+ if (clientY === null) {
+ return;
+ }
+
+ dragStartYRef.current = clientY;
+ dragStartPositionRef.current = handlePosition;
+ didDragRef.current = false;
+ setIsDragging(false);
+ setIsPointerDown(true);
+ }, [handlePosition]);
+
+ // Persist drag-handle position so users keep their preferred quick-access location.
+ useEffect(() => {
+ localStorage.setItem(
+ HANDLE_POSITION_STORAGE_KEY,
+ JSON.stringify({ y: handlePosition }),
+ );
+ }, [handlePosition]);
+
+ useEffect(() => {
+ if (!isPointerDown) {
+ return undefined;
+ }
+
+ const handleMouseMove = (event: MouseEvent) => {
+ handleMove(event);
+ };
+ const handleMouseUp = () => {
+ endDrag();
+ };
+ const handleTouchMove = (event: TouchEvent) => {
+ handleMove(event);
+ };
+ const handleTouchEnd = () => {
+ endDrag();
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ document.addEventListener('touchmove', handleTouchMove, { passive: false });
+ document.addEventListener('touchend', handleTouchEnd);
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.removeEventListener('touchmove', handleTouchMove);
+ document.removeEventListener('touchend', handleTouchEnd);
+ };
+ }, [endDrag, handleMove, isPointerDown]);
+
+ useEffect(() => (
+ () => {
+ clearBodyDragStyles();
+ }
+ ), [clearBodyDragStyles]);
+
+ const consumeSuppressedClick = useCallback((): boolean => {
+ if (!suppressNextClickRef.current) {
+ return false;
+ }
+
+ suppressNextClickRef.current = false;
+ return true;
+ }, []);
+
+ const handleStyle = useMemo(() => {
+ if (!isMobile || typeof window === 'undefined') {
+ return {
+ top: `${handlePosition}%`,
+ transform: 'translateY(-50%)',
+ };
+ }
+
+ return {
+ bottom: `${(window.innerHeight * handlePosition) / 100}px`,
+ };
+ }, [handlePosition, isMobile]);
+
+ return {
+ isDragging,
+ handleStyle,
+ startDrag,
+ endDrag,
+ consumeSuppressedClick,
+ };
+}
diff --git a/src/components/quick-settings-panel/hooks/useWhisperMode.ts b/src/components/quick-settings-panel/hooks/useWhisperMode.ts
new file mode 100644
index 00000000..eeda67ce
--- /dev/null
+++ b/src/components/quick-settings-panel/hooks/useWhisperMode.ts
@@ -0,0 +1,59 @@
+import { useCallback, useState } from 'react';
+import {
+ VIBE_MODE_ALIASES,
+ WHISPER_MODE_CHANGED_EVENT,
+ WHISPER_MODE_STORAGE_KEY,
+} from '../constants';
+import type { WhisperMode, WhisperOptionValue } from '../types';
+
+const ALL_VALID_MODES: WhisperMode[] = [
+ 'default',
+ 'prompt',
+ 'vibe',
+ 'instructions',
+ 'architect',
+];
+
+const isWhisperMode = (value: string): value is WhisperMode => (
+ ALL_VALID_MODES.includes(value as WhisperMode)
+);
+
+const readStoredMode = (): WhisperMode => {
+ if (typeof window === 'undefined') {
+ return 'default';
+ }
+
+ const storedValue = localStorage.getItem(WHISPER_MODE_STORAGE_KEY);
+ if (!storedValue) {
+ return 'default';
+ }
+
+ return isWhisperMode(storedValue) ? storedValue : 'default';
+};
+
+export function useWhisperMode() {
+ const [whisperMode, setWhisperModeState] = useState(readStoredMode);
+
+ const setWhisperMode = useCallback((value: WhisperOptionValue) => {
+ setWhisperModeState(value);
+ localStorage.setItem(WHISPER_MODE_STORAGE_KEY, value);
+ window.dispatchEvent(new Event(WHISPER_MODE_CHANGED_EVENT));
+ }, []);
+
+ const isOptionSelected = useCallback(
+ (value: WhisperOptionValue) => {
+ if (value === 'vibe') {
+ return VIBE_MODE_ALIASES.includes(whisperMode);
+ }
+
+ return whisperMode === value;
+ },
+ [whisperMode],
+ );
+
+ return {
+ whisperMode,
+ setWhisperMode,
+ isOptionSelected,
+ };
+}
diff --git a/src/components/quick-settings-panel/types.ts b/src/components/quick-settings-panel/types.ts
new file mode 100644
index 00000000..4a12fc01
--- /dev/null
+++ b/src/components/quick-settings-panel/types.ts
@@ -0,0 +1,35 @@
+import type { CSSProperties } from 'react';
+import type { LucideIcon } from 'lucide-react';
+
+export type PreferenceToggleKey =
+ | 'autoExpandTools'
+ | 'showRawParameters'
+ | 'showThinking'
+ | 'autoScrollToBottom'
+ | 'sendByCtrlEnter';
+
+export type QuickSettingsPreferences = Record;
+
+export type PreferenceToggleItem = {
+ key: PreferenceToggleKey;
+ labelKey: string;
+ icon: LucideIcon;
+};
+
+export type WhisperMode =
+ | 'default'
+ | 'prompt'
+ | 'vibe'
+ | 'instructions'
+ | 'architect';
+
+export type WhisperOptionValue = 'default' | 'prompt' | 'vibe';
+
+export type WhisperOption = {
+ value: WhisperOptionValue;
+ titleKey: string;
+ descriptionKey: string;
+ icon: LucideIcon;
+};
+
+export type QuickSettingsHandleStyle = CSSProperties;
diff --git a/src/components/quick-settings-panel/view/QuickSettingsContent.tsx b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx
new file mode 100644
index 00000000..3075716b
--- /dev/null
+++ b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx
@@ -0,0 +1,82 @@
+import { Moon, Sun } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { DarkModeToggle } from '../../../shared/view/ui';
+import LanguageSelector from '../../LanguageSelector.jsx';
+import {
+ INPUT_SETTING_TOGGLES,
+ SETTING_ROW_CLASS,
+ TOOL_DISPLAY_TOGGLES,
+ VIEW_OPTION_TOGGLES,
+} from '../constants';
+import type {
+ PreferenceToggleItem,
+ PreferenceToggleKey,
+ QuickSettingsPreferences,
+} from '../types';
+import QuickSettingsSection from './QuickSettingsSection';
+import QuickSettingsToggleRow from './QuickSettingsToggleRow';
+import QuickSettingsWhisperSection from './QuickSettingsWhisperSection';
+
+type QuickSettingsContentProps = {
+ isDarkMode: boolean;
+ isMobile: boolean;
+ preferences: QuickSettingsPreferences;
+ onPreferenceChange: (key: PreferenceToggleKey, value: boolean) => void;
+};
+
+export default function QuickSettingsContent({
+ isDarkMode,
+ isMobile,
+ preferences,
+ onPreferenceChange,
+}: QuickSettingsContentProps) {
+ const { t } = useTranslation('settings');
+
+ const renderToggleRows = (items: PreferenceToggleItem[]) => (
+ items.map(({ key, labelKey, icon }) => (
+ onPreferenceChange(key, value)}
+ />
+ ))
+ );
+
+ return (
+
+
+
+
+ {isDarkMode ? (
+
+ ) : (
+
+ )}
+ {t('quickSettings.darkMode')}
+
+
+
+
+
+
+
+ {renderToggleRows(TOOL_DISPLAY_TOGGLES)}
+
+
+
+ {renderToggleRows(VIEW_OPTION_TOGGLES)}
+
+
+
+ {renderToggleRows(INPUT_SETTING_TOGGLES)}
+
+ {t('quickSettings.sendByCtrlEnterDescription')}
+
+
+
+
+
+ );
+}
diff --git a/src/components/quick-settings-panel/view/QuickSettingsHandle.tsx b/src/components/quick-settings-panel/view/QuickSettingsHandle.tsx
new file mode 100644
index 00000000..98141479
--- /dev/null
+++ b/src/components/quick-settings-panel/view/QuickSettingsHandle.tsx
@@ -0,0 +1,74 @@
+import type {
+ MouseEvent as ReactMouseEvent,
+ TouchEvent as ReactTouchEvent,
+} from 'react';
+import {
+ ChevronLeft,
+ ChevronRight,
+ GripVertical,
+} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import type { QuickSettingsHandleStyle } from '../types';
+
+type QuickSettingsHandleProps = {
+ isOpen: boolean;
+ isDragging: boolean;
+ style: QuickSettingsHandleStyle;
+ onClick: (event: ReactMouseEvent) => void;
+ onMouseDown: (event: ReactMouseEvent) => void;
+ onTouchStart: (event: ReactTouchEvent) => void;
+};
+
+export default function QuickSettingsHandle({
+ isOpen,
+ isDragging,
+ style,
+ onClick,
+ onMouseDown,
+ onTouchStart,
+}: QuickSettingsHandleProps) {
+ const { t } = useTranslation('settings');
+
+ const placementClass = isOpen ? 'right-64' : 'right-0';
+ const borderClass = isDragging
+ ? 'border-blue-500 dark:border-blue-400'
+ : 'border-gray-200 dark:border-gray-700';
+ const transitionClass = isDragging
+ ? ''
+ : 'transition-all duration-150 ease-out';
+ const cursorClass = isDragging ? 'cursor-grabbing' : 'cursor-pointer';
+ const ariaLabel = isDragging
+ ? t('quickSettings.dragHandle.dragging')
+ : isOpen
+ ? t('quickSettings.dragHandle.closePanel')
+ : t('quickSettings.dragHandle.openPanel');
+ const title = isDragging
+ ? t('quickSettings.dragHandle.draggingStatus')
+ : t('quickSettings.dragHandle.toggleAndMove');
+
+ return (
+
+ );
+}
diff --git a/src/components/quick-settings-panel/view/QuickSettingsPanelHeader.tsx b/src/components/quick-settings-panel/view/QuickSettingsPanelHeader.tsx
new file mode 100644
index 00000000..e24b75b6
--- /dev/null
+++ b/src/components/quick-settings-panel/view/QuickSettingsPanelHeader.tsx
@@ -0,0 +1,15 @@
+import { Settings2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+export default function QuickSettingsPanelHeader() {
+ const { t } = useTranslation('settings');
+
+ return (
+
+
+
+ {t('quickSettings.title')}
+
+
+ );
+}
diff --git a/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx b/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx
new file mode 100644
index 00000000..17b26ff9
--- /dev/null
+++ b/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx
@@ -0,0 +1,91 @@
+import { useCallback, useMemo, useState } from 'react';
+import type { MouseEvent as ReactMouseEvent } from 'react';
+import { useDeviceSettings } from '../../../hooks/useDeviceSettings';
+import { useUiPreferences } from '../../../hooks/useUiPreferences';
+import { useTheme } from '../../../contexts/ThemeContext';
+import { useQuickSettingsDrag } from '../hooks/useQuickSettingsDrag';
+import type { PreferenceToggleKey, QuickSettingsPreferences } from '../types';
+import QuickSettingsContent from './QuickSettingsContent';
+import QuickSettingsHandle from './QuickSettingsHandle';
+import QuickSettingsPanelHeader from './QuickSettingsPanelHeader';
+
+export default function QuickSettingsPanelView() {
+ const [isOpen, setIsOpen] = useState(false);
+ const { isMobile } = useDeviceSettings({ trackPWA: false });
+ const { isDarkMode } = useTheme();
+ const { preferences, setPreference } = useUiPreferences();
+ const {
+ isDragging,
+ handleStyle,
+ startDrag,
+ consumeSuppressedClick,
+ } = useQuickSettingsDrag({ isMobile });
+
+ const quickSettingsPreferences = useMemo(() => ({
+ autoExpandTools: preferences.autoExpandTools,
+ showRawParameters: preferences.showRawParameters,
+ showThinking: preferences.showThinking,
+ autoScrollToBottom: preferences.autoScrollToBottom,
+ sendByCtrlEnter: preferences.sendByCtrlEnter,
+ }), [
+ preferences.autoExpandTools,
+ preferences.autoScrollToBottom,
+ preferences.sendByCtrlEnter,
+ preferences.showRawParameters,
+ preferences.showThinking,
+ ]);
+
+ const handlePreferenceChange = useCallback(
+ (key: PreferenceToggleKey, value: boolean) => {
+ setPreference(key, value);
+ },
+ [setPreference],
+ );
+
+ const handleToggleFromHandle = useCallback(
+ (event: ReactMouseEvent) => {
+ // A drag releases a click event as well; this guard prevents accidental toggles.
+ if (consumeSuppressedClick()) {
+ event.preventDefault();
+ return;
+ }
+
+ setIsOpen((previous) => !previous);
+ },
+ [consumeSuppressedClick],
+ );
+
+ return (
+ <>
+
+
+
+
+ {isOpen && (
+ setIsOpen(false)}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/components/quick-settings-panel/view/QuickSettingsSection.tsx b/src/components/quick-settings-panel/view/QuickSettingsSection.tsx
new file mode 100644
index 00000000..07531bec
--- /dev/null
+++ b/src/components/quick-settings-panel/view/QuickSettingsSection.tsx
@@ -0,0 +1,22 @@
+import type { ReactNode } from 'react';
+
+type QuickSettingsSectionProps = {
+ title: string;
+ children: ReactNode;
+ className?: string;
+};
+
+export default function QuickSettingsSection({
+ title,
+ children,
+ className = '',
+}: QuickSettingsSectionProps) {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
diff --git a/src/components/quick-settings-panel/view/QuickSettingsToggleRow.tsx b/src/components/quick-settings-panel/view/QuickSettingsToggleRow.tsx
new file mode 100644
index 00000000..61bdd52c
--- /dev/null
+++ b/src/components/quick-settings-panel/view/QuickSettingsToggleRow.tsx
@@ -0,0 +1,34 @@
+import { memo } from 'react';
+import type { LucideIcon } from 'lucide-react';
+import { CHECKBOX_CLASS, TOGGLE_ROW_CLASS } from '../constants';
+
+type QuickSettingsToggleRowProps = {
+ label: string;
+ icon: LucideIcon;
+ checked: boolean;
+ onCheckedChange: (checked: boolean) => void;
+};
+
+function QuickSettingsToggleRow({
+ label,
+ icon: Icon,
+ checked,
+ onCheckedChange,
+}: QuickSettingsToggleRowProps) {
+ return (
+
+ );
+}
+
+export default memo(QuickSettingsToggleRow);
diff --git a/src/components/quick-settings-panel/view/QuickSettingsWhisperSection.tsx b/src/components/quick-settings-panel/view/QuickSettingsWhisperSection.tsx
new file mode 100644
index 00000000..297dbdb6
--- /dev/null
+++ b/src/components/quick-settings-panel/view/QuickSettingsWhisperSection.tsx
@@ -0,0 +1,44 @@
+import { useTranslation } from 'react-i18next';
+import { TOGGLE_ROW_CLASS, WHISPER_OPTIONS } from '../constants';
+import { useWhisperMode } from '../hooks/useWhisperMode';
+import QuickSettingsSection from './QuickSettingsSection';
+
+export default function QuickSettingsWhisperSection() {
+ const { t } = useTranslation('settings');
+ const { setWhisperMode, isOptionSelected } = useWhisperMode();
+
+ return (
+ // This section stays hidden intentionally until dictation modes are reintroduced.
+
+
+ {WHISPER_OPTIONS.map(({ value, icon: Icon, titleKey, descriptionKey }) => (
+
+ ))}
+
+
+ );
+}