diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx
index 1872d1f23..2e71c1bd1 100644
--- a/src/apps/chat/components/composer/Composer.tsx
+++ b/src/apps/chat/components/composer/Composer.tsx
@@ -2,9 +2,8 @@ import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import type { FileWithHandle } from 'browser-fs-access';
-import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Typography } from '@mui/joy';
-import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
-import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
+import type { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
+import { Box, Button, ButtonGroup, Card, Grid, IconButton, Textarea, Typography } from '@mui/joy';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import PsychologyIcon from '@mui/icons-material/Psychology';
import SendIcon from '@mui/icons-material/Send';
@@ -57,14 +56,10 @@ import { providerStarredMessages, StarredMessageItem } from './actile/providerSt
import { useActileManager } from './actile/useActileManager';
import type { AttachmentDraftId, AttachmentDraftsAction } from '~/common/attachment-drafts/attachment.types';
-import { ButtonAttachCameraMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachCamera';
-import { ButtonAttachClipboardMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachClipboard';
-import { ButtonAttachGoogleDriveMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachGoogleDrive';
-import { ButtonAttachScreenCaptureMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachScreenCapture';
-import { ButtonAttachWebMemo } from '~/common/attachment-drafts/attachment-sources/ButtonAttachWeb';
-import { hasGoogleDriveCapability, useGoogleDrivePicker } from '~/common/attachment-drafts/attachment-sources/useGoogleDrivePicker';
+import { AttachmentSourcesMemo } from '~/common/attachment-drafts/attachment-sources/AttachmentSources';
import { useAttachmentDrafts } from '~/common/attachment-drafts/useAttachmentDrafts';
import { useAttachmentDraftsEnrichment } from '~/common/attachment-drafts/llm-enrichment/useAttachmentDraftsEnrichment';
+import { useGoogleDrivePicker } from '~/common/attachment-drafts/attachment-sources/useGoogleDrivePicker';
import { useWebAttachmentModal } from '~/common/attachment-drafts/attachment-sources/useWebAttachmentModal';
import type { ChatExecuteMode } from '../../execute-mode/execute-mode.types';
@@ -791,42 +786,24 @@ export function Composer(props: {
{/* [mobile] Mic button */}
{recognitionState.isAvailable && }
- {/* Responsive Camera OCR button */}
- {showChatAttachments && }
-
{/* [mobile] Attach file button (in draw with image mode) */}
{showChatAttachments === 'only-images' && }
- {/* [mobile] [+] button */}
+ {/* [mobile] [+] attachment sources menu */}
{showChatAttachments === true && (
-
-
-
-
-
-
+
)}
{/* [Mobile] MultiChat button */}
@@ -837,31 +814,26 @@ export function Composer(props: {
{/* [Desktop, Col1] Insert Multi-modal content buttons */}
{isDesktop && showChatAttachments && (
-
+
- {/**/}
- {/* Attach*/}
- {/**/}
+ {/* [desktop] Attachment Sources: inline buttons */}
+
- {/* Responsive Open Files button */}
-
-
- {/* Responsive Web button */}
- {showChatAttachments !== 'only-images' && }
-
- {/* Responsive Google Drive button */}
- {hasGoogleDriveCapability && showChatAttachments !== 'only-images' && }
-
- {/* Responsive Paste button */}
- {supportsClipboardRead() && showChatAttachments !== 'only-images' && }
-
- {/* Responsive Screen Capture button */}
- {labsAttachScreenCapture && supportsScreenCapture && }
-
- {/* Responsive Camera OCR button */}
- {labsCameraDesktop && supportsCameraCapture() && }
-
- )}
+
+ )}
{/* Top: Textarea & Mic & Overlays, Bottom, Attachment Drafts */}
diff --git a/src/common/attachment-drafts/attachment-sources/AttachmentSources.tsx b/src/common/attachment-drafts/attachment-sources/AttachmentSources.tsx
new file mode 100644
index 000000000..6a9742d7e
--- /dev/null
+++ b/src/common/attachment-drafts/attachment-sources/AttachmentSources.tsx
@@ -0,0 +1,440 @@
+import * as React from 'react';
+import { keyframes } from '@emotion/react';
+import type { FileWithHandle } from 'browser-fs-access';
+
+import { Box, Button, Checkbox, ColorPaletteProp, Divider, Dropdown, IconButton, ListItem, ListItemDecorator, Menu, MenuButton, MenuItem } from '@mui/joy';
+import AddRoundedIcon from '@mui/icons-material/AddRounded';
+import AddToDriveRoundedIcon from '@mui/icons-material/AddToDriveRounded';
+import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
+import CameraAltOutlinedIcon from '@mui/icons-material/CameraAltOutlined';
+import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo';
+import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded';
+import ScreenshotMonitorIcon from '@mui/icons-material/ScreenshotMonitor';
+
+import { useBrowseStore } from '~/modules/browse/store-module-browsing';
+
+import { ButtonAttachFilesMemo, openFileForAttaching } from '~/common/components/ButtonAttachFiles';
+import { supportsClipboardRead } from '~/common/util/clipboardUtils';
+import { takeScreenCapture } from '~/common/util/screenCaptureUtils';
+
+import { ButtonAttachCameraMemo } from './ButtonAttachCamera';
+import { ButtonAttachClipboardMemo } from './ButtonAttachClipboard';
+import { ButtonAttachGoogleDriveMemo } from './ButtonAttachGoogleDrive';
+import { ButtonAttachScreenCaptureMemo } from './ButtonAttachScreenCapture';
+import { ButtonAttachWebMemo } from './ButtonAttachWeb';
+import { hasGoogleDriveCapability } from './useGoogleDrivePicker';
+
+
+// configuration
+export const ATTACH_BUTTON_RADIUS = '18px'; // for the rich (non-compact) menu button
+
+
+// animations for the rich (non-compact) menu
+const animationMenu = keyframes` from {opacity: 0;} to {opacity: 1;}`;
+const animationMenuItem = keyframes` from {opacity: 0;transform: translateY(-6px);} to {opacity: 1;transform: translateY(0);}`;
+
+const _style = {
+ menuItem: {
+ py: 1,
+ // pl: 3,
+ // pr: 2,
+ minHeight: 60,
+ } as const,
+ menuItemContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 0.125,
+ } as const,
+ menuItemName: {
+ typography: 'title-sm',
+ fontWeight: 600,
+ // fontSize: '15px',
+ } as const,
+ menuItemDescription: {
+ fontSize: 'xs',
+ color: 'text.tertiary',
+ fontWeight: 400,
+ } as const,
+};
+
+
+// Rich menu item (used in menu-rich mode)
+function RichMenuItem(props: {
+ name: React.ReactNode;
+ description: React.ReactNode;
+ icon: React.ReactNode;
+ onClick: () => void;
+ delay?: number;
+ disabled?: boolean;
+ color?: ColorPaletteProp;
+}) {
+ return (
+
+ );
+}
+
+
+// Auto-download toggle (shown when browsing capability exists)
+function AutoDownloadToggle(props: { delay?: number }) {
+
+ // external state
+ const enableComposerAttach = useBrowseStore(s => s.enableComposerAttach);
+
+ const handleToggle = React.useCallback((event: React.ChangeEvent) => {
+ event.stopPropagation();
+ useBrowseStore.getState().setEnableComposerAttach(event.target.checked);
+ }, []);
+
+ return <>
+
+
+
+ {
+ // event.preventDefault();
+ // event.stopPropagation();
+ // setEnableComposerAttach(!enableComposerAttach);
+ // }}
+ >
+
+ event.stopPropagation()}
+ sx={{ ml: 0.375 }}
+ />
+
+
+
+ Attach pasted URLs
+
+
+ Download and attach pasted web links
+
+
+
+ >;
+}
+
+
+/**
+ * Portable attachment sources component.
+ *
+ * Three modes:
+ * - **menu-compact**: Mobile-style — icon trigger, simple MenuItems (no descriptions/animations)
+ * - **menu-rich**: Desktop-style — labeled button trigger, rich items with descriptions and animations
+ * - **inline-buttons**: Individual source buttons rendered inline (no dropdown)
+ */
+export const AttachmentSourcesMemo = React.memo(AttachmentSources);
+
+function AttachmentSources(props: {
+ // mode
+ mode: 'menu-compact' | 'menu-rich' | 'inline-buttons',
+ color?: ColorPaletteProp, // menu-rich and inline-buttons
+ richButtonStandOut?: boolean, // menu-rich only
+ // source availability - note that hasGoogleDriveCapability is local
+ canBrowse: boolean, // whether browsing is available (for Web button and showing the auto-attach toggle)
+ hasCamera: boolean,
+ // hasGoogleDrive: boolean, // it's now local: hasGoogleDriveCapability
+ hasScreenCapture: boolean,
+ // configuration
+ onlyImages?: boolean, // makes clipboard/drive/web unavailable
+ // callbacks
+ onAttachClipboard: () => void,
+ onAttachFiles: (files: FileWithHandle[], errorMessage: string | null) => void,
+ onAttachScreenCapture: (file: File) => void,
+ onOpenCamera: () => void,
+ onOpenGoogleDrivePicker?: () => void, // optional because requires additional external setup (e.g. user-storage of tokens)
+ onOpenWebInput: () => void,
+}) {
+
+ // state (screen capture — used in menu modes where the component handles the capture)
+ const [capturingScreen, setCapturingScreen] = React.useState(false);
+ const [screenCaptureError, setScreenCaptureError] = React.useState(null);
+
+
+ // handlers
+
+ const { onAttachFiles, onAttachScreenCapture } = props;
+
+ const handleAttachFilePicker = React.useCallback(() => {
+ return openFileForAttaching(true, onAttachFiles);
+ }, [onAttachFiles]);
+
+ const handleTakeScreenCapture = React.useCallback(async () => {
+ setScreenCaptureError(null);
+ setCapturingScreen(true);
+ try {
+ const file = await takeScreenCapture();
+ file && onAttachScreenCapture(file);
+ } catch (error: any) {
+ const message = error instanceof Error ? error.message : String(error);
+ setScreenCaptureError(message);
+ }
+ setCapturingScreen(false);
+ }, [onAttachScreenCapture]);
+
+
+ // inline-buttons mode — individual buttons rendered flat (no dropdown)
+ if (props.mode === 'inline-buttons')
+ return <>
+
+ {/* Files */}
+
+
+ {/* Web */}
+ {!props.onlyImages && }
+
+ {/* Google Drive */}
+ {hasGoogleDriveCapability && !props.onlyImages && !!props.onOpenGoogleDrivePicker && (
+
+ )}
+
+ {/* Clipboard */}
+ {supportsClipboardRead() && !props.onlyImages && (
+
+ )}
+
+ {/* Screen Capture */}
+ {props.hasScreenCapture && (
+
+ )}
+
+ {/* Camera */}
+ {props.hasCamera && (
+
+ )}
+
+ >;
+
+
+ // menu-compact mode (mobile) — simple icon trigger with flat menu items
+ if (props.mode === 'menu-compact')
+ return <>
+
+
+
+
+
+
+
+
+ {/* [mobile] Responsive Camera OCR button */}
+ {props.hasCamera && }
+
+ >;
+
+
+ // menu-rich mode (desktop) — labeled button trigger with animated, descriptive menu items
+ return (
+
+ ,
+ fullWidth: true, // to match other buttons in the col
+ sx: {
+ minWidth: 100,
+ justifyContent: 'flex-start',
+ borderRadius: ATTACH_BUTTON_RADIUS,
+ textWrap: 'nowrap',
+ ...(props.richButtonStandOut && {
+ backgroundColor: 'background.popup',
+ border: '1px solid',
+ borderColor: `${props.color || 'neutral'}.outlinedBorder`,
+ }),
+ // when aria-expanded is true (menu open), remove top border radius
+ '&[aria-expanded="true"]': {
+ borderTopRightRadius: 0,
+ borderTopLeftRadius: 0,
+ backgroundColor: `${props.color || 'neutral'}.softHoverBg`,
+ },
+ },
+ },
+ }}
+ >
+ Attach
+
+
+
+
+ );
+}